Hi Postgres hackers,
This is my first patch to the project and I've been sitting on it for 6 months!
This patch was produced via:
git diff -p -U 4d936c3fff1ac8dead2cc240ba3da2ed6337257c
The branch point was 4d936c3fff1ac8dead2cc240ba3da2ed6337257c (master
as of 05/12/2025 1445 GMT)
The patch, though a single diff, was generated from 7 logically
distinct commits (feature, tests, expected output etc.).
I hope I've read the submission guides sufficiently. The code change
was based heavily on the existing code in indxpath.c.
Here's a summary of the feature:
Prior to this patch, only BitmapOr paths were considered for partial
indexes. With this patch, we now support ScalarArrayOpExpr clauses
too (i.e. ANY() and IN()).
I found no entry for this feature in the TODO list here;
- https://wiki.postgresql.org/wiki/Todo
However, it has previously been reported/raised here;
-
https://www.postgresql.org/message-id/flat/c128bd06-a246-4129-914c-3dee7b13417a%40vondra.me#5b3f3b7e90d6de8c39a095afaea6b460
The new function, generate_bitmap_saop_paths, was largely based on the
existing generate_bitmap_or_paths() function while also glancing at
other array handling code such as that found in backend/utils/adt/xml.c
plus some additional false-starts in backend/optimizer/util/predtest.c
The C code was formatted via;
src/tools/pgindent/pgindent --indent=src/tools/pg_bsd_indent/pg_bsd_indent
Cheers,
Jim Vanns
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 2654c59c4c6..96c39ef738e 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -36,12 +36,23 @@
#include "optimizer/restrictinfo.h"
#include "utils/lsyscache.h"
#include "utils/selfuncs.h"
+#include "utils/array.h"
/* XXX see PartCollMatchesExprColl */
#define IndexCollMatchesExprColl(idxcollation, exprcollation) \
((idxcollation) == InvalidOid || (idxcollation) == (exprcollation))
+/*
+ * Proof attempts involving large arrays in ScalarArrayOpExpr nodes are
+ * likely to require O(N^2) time, and more often than not fail anyway.
+ * So we set an arbitrary limit on the number of array elements that
+ * we will allow to be treated as an AND or OR clause.
+ * XXX is it worth exposing this as a GUC knob?
+ * This block was copied verbatim from ../utils/predtest.c
+ */
+#define MAX_SAOP_ARRAY_SIZE 100
+
/* Whether we are looking for plain indexscan, bitmap scan, or either */
typedef enum
{
@@ -113,6 +124,8 @@ static List *build_paths_for_OR(PlannerInfo *root, RelOptInfo *rel,
List *clauses, List *other_clauses);
static List *generate_bitmap_or_paths(PlannerInfo *root, RelOptInfo *rel,
List *clauses, List *other_clauses);
+static List *generate_bitmap_saop_paths(PlannerInfo *root, RelOptInfo *rel,
+ List *clauses, List *indexes);
static Path *choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel,
List *paths);
static int path_usage_comparator(const void *a, const void *b);
@@ -323,6 +336,15 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel)
rel->baserestrictinfo, NIL);
bitindexpaths = list_concat(bitindexpaths, indexpaths);
+ /*
+ * Now generate BitmapOrPaths for any suitable SAOP-clauses present in the
+ * restriction list. Add these to bitindexpaths.
+ */
+ indexpaths = generate_bitmap_saop_paths(root, rel,
+ rel->baserestrictinfo,
+ rel->indexlist);
+ bitindexpaths = list_concat(bitindexpaths, indexpaths);
+
/*
* Likewise, generate BitmapOrPaths for any suitable OR-clauses present in
* the joinclause list. Add these to bitjoinpaths.
@@ -1770,6 +1792,203 @@ generate_bitmap_or_paths(PlannerInfo *root, RelOptInfo *rel,
}
+/*
+ * generate_bitmap_saop_paths
+ * Look through the list of clauses to find IN/ANY clauses, and generate
+ * a BitmapOrPath for each one we can handle via an index. Return a list
+ * of the generated BitmapOrPaths.
+ */
+static List *
+generate_bitmap_saop_paths(PlannerInfo *root, RelOptInfo *rel,
+ List *clauses, List *indexes)
+{
+ ListCell *lc;
+ List *result = NIL;
+
+ /*
+ * Iterate through each restriction clause to find ScalarArrayOpExprs.
+ */
+ foreach(lc, clauses)
+ {
+ bool elem_typbyval;
+ char elem_typalign;
+ int16 elem_typlen;
+ int nelems = 0;
+ Oid elem_type;
+ Node *leftop;
+ Node *rightop;
+ const Const *arrayconst;
+ ArrayType *arrayval;
+ Datum *elem_values;
+ bool *elem_nulls;
+ ScalarArrayOpExpr *saop;
+ List *per_saop_paths = NIL;
+ RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc);
+
+ /* We only care about ScalarArrayOpExpr */
+ if (!IsA(rinfo->clause, ScalarArrayOpExpr))
+ continue;
+
+ saop = (ScalarArrayOpExpr *) rinfo->clause;
+
+ /* We only handle IN clauses (operator ANY), not ALL clauses. */
+ if (!saop->useOr)
+ continue;
+
+ /*
+ * The logic to build bitmap paths requires concrete values.
+ * Therefore, we can only handle the case where the array is a
+ * non-null Const.
+ */
+ leftop = (Node *) linitial(saop->args);
+ rightop = (Node *) lsecond(saop->args);
+ if (!rightop || !IsA(rightop, Const) || ((Const *) rightop)->constisnull)
+ continue;
+
+ arrayconst = (const Const *) rightop;
+
+ /*
+ * Deconstruct the array constant into its constituent elements.
+ */
+ arrayval = DatumGetArrayTypeP(arrayconst->constvalue);
+ if (ARR_NDIM(arrayval) != 1)
+ continue;
+
+ /* Safety valve for huge IN lists */
+ nelems = ArrayGetNItems(1, ARR_DIMS(arrayval));
+ if (nelems < 1 || nelems > MAX_SAOP_ARRAY_SIZE)
+ continue;
+
+ elem_type = ARR_ELEMTYPE(arrayval);
+ get_typlenbyvalalign(elem_type,
+ &elem_typlen, &elem_typbyval, &elem_typalign);
+
+ deconstruct_array(arrayval, elem_type,
+ elem_typlen, elem_typbyval, elem_typalign,
+ &elem_values, &elem_nulls, &nelems);
+
+ /*
+ * For each element of the IN list, try to find a matching partial
+ * index and generate a bitmap path for it.
+ */
+ for (int i = 0; i < nelems; i++)
+ {
+ ListCell *idx_lc;
+ Expr *elem_const;
+ OpExpr *opclause;
+ RestrictInfo *new_rinfo;
+ bool found_path_for_elem = false;
+ const Datum elem_value = elem_values[i];
+
+ /* We can't build an index qual from a NULL element. */
+ if (elem_nulls[i])
+ break;
+
+ /*
+ * Construct a new clause: "indexkey = const_element".
+ */
+ elem_const = (Expr *) makeConst(elem_type,
+ -1,
+ arrayconst->constcollid,
+ elem_typlen,
+ elem_value, false, elem_typbyval);
+
+ opclause = (OpExpr *) make_opclause(saop->opno, BOOLOID, false,
+ (Expr *) copyObject(leftop),
+ elem_const,
+ saop->inputcollid, saop->inputcollid);
+
+ new_rinfo = make_simple_restrictinfo(root, (Expr *) opclause);
+
+ /*
+ * Search through all available indexes to find a partial index
+ * that is satisfied by our newly constructed clause.
+ */
+ foreach(idx_lc, indexes)
+ {
+ IndexOptInfo *index = (IndexOptInfo *) lfirst(idx_lc);
+
+ /*
+ * Ignore non-partial indexes or indexes that don't support
+ * bitmap scans
+ */
+ if (index->indpred == NIL || !index->amhasgetbitmap)
+ continue;
+
+ /*
+ * If the index predicate is implied by our new clause, we can
+ * use it
+ */
+ if (predicate_implied_by(index->indpred, list_make1(new_rinfo), false))
+ {
+ /*
+ * We found a usable index. Now build a bitmap path for it
+ * using the new clause as the index qual.
+ */
+ List *indexpaths;
+ IndexClauseSet clauseset;
+
+ MemSet(&clauseset, 0, sizeof(clauseset));
+ match_clause_to_index(root, new_rinfo, index, &clauseset);
+
+ /* We must have found a match to proceed */
+ if (!clauseset.nonempty)
+ continue;
+
+ indexpaths = build_index_paths(root, rel,
+ index, &clauseset,
+ true, /* predicate is useful */
+ ST_BITMAPSCAN,
+ NULL);
+
+ if (indexpaths)
+ {
+ /*
+ * Success! Add the path and stop searching for other
+ * indexes for this element. We only need one.
+ */
+ per_saop_paths = list_concat(per_saop_paths, indexpaths);
+ found_path_for_elem = true;
+ break; /* out of inner index loop */
+ }
+ }
+ }
+
+ /*
+ * If we could not find any partial index for this element, then
+ * we cannot satisfy the whole IN clause this way. Abort.
+ */
+ if (!found_path_for_elem)
+ {
+ per_saop_paths = NIL; /* Discard any paths found so far */
+ break; /* out of outer element loop */
+ }
+ }
+
+ /*
+ * If we successfully found a path for every element of the IN list,
+ * we can combine them into a BitmapOrPath.
+ */
+ if (per_saop_paths != NIL)
+ {
+ Path *bitmapqual = list_length(per_saop_paths) > 1 ?
+ (Path *) create_bitmap_or_path(root, rel, per_saop_paths) :
+ (Path *) linitial(per_saop_paths);
+
+ result = lappend(result, bitmapqual);
+ }
+
+ if (elem_values)
+ pfree(elem_values);
+
+ if (elem_nulls)
+ pfree(elem_nulls);
+ }
+
+ return result;
+}
+
+
/*
* choose_bitmap_and
* Given a nonempty list of bitmap paths, AND them into one path.
diff --git a/src/test/regress/expected/saop_bitmap-1.out b/src/test/regress/expected/saop_bitmap-1.out
new file mode 100644
index 00000000000..96fd5c058d7
--- /dev/null
+++ b/src/test/regress/expected/saop_bitmap-1.out
@@ -0,0 +1,94 @@
+-- Test ScalarArrayOpExpr for partial indexes
+-- Generate enough data that we can test the use of an index
+CREATE TABLE tpairs(
+ key TEXT,
+ value int
+)
+WITH (autovacuum_enabled = false);
+-- Install the partial index on these values
+CREATE INDEX ON tpairs(key) WHERE key = 'foo';
+CREATE INDEX ON tpairs(key) WHERE key = 'bar';
+CREATE INDEX ON tpairs(key) WHERE key = 'baz';
+-- Dummy data to surround ours
+INSERT INTO tpairs(key, value)
+SELECT n::TEXT, n
+FROM generate_series(1, 10000) AS tmp(n);
+-- Our specific test data to force the partial index
+INSERT INTO tpairs(key, value)
+VALUES
+('foo', 25),
+('bar', 50),
+('baz', 75);
+-- Update statistics
+VACUUM (ANALYZE, FREEZE) tpairs;
+-- We want ensure that an index use is always attempted - just for the test
+SET enable_seqscan=false;
+-- Variant 1 (works without this patch)
+EXPLAIN (COSTS OFF)
+SELECT value
+FROM tpairs
+WHERE
+value > 10 AND
+(
+ key = 'foo' OR
+ key = 'bar' OR
+ key = 'baz'
+);
+ QUERY PLAN
+-------------------------------------------------------------------------------------
+ Bitmap Heap Scan on tpairs
+ Recheck Cond: ((key = 'foo'::text) OR (key = 'bar'::text) OR (key = 'baz'::text))
+ Filter: (value > 10)
+ -> BitmapOr
+ -> Bitmap Index Scan on tpairs_key_idx
+ Index Cond: (key = 'foo'::text)
+ -> Bitmap Index Scan on tpairs_key_idx1
+ Index Cond: (key = 'bar'::text)
+ -> Bitmap Index Scan on tpairs_key_idx2
+ Index Cond: (key = 'baz'::text)
+(10 rows)
+
+-- Variant 2 (works only with this patch)
+EXPLAIN (COSTS OFF)
+SELECT value
+FROM tpairs
+WHERE
+value > 10 AND
+key IN ('foo', 'bar', 'baz');
+ QUERY PLAN
+-------------------------------------------------------------------------------------
+ Bitmap Heap Scan on tpairs
+ Recheck Cond: ((key = 'foo'::text) OR (key = 'bar'::text) OR (key = 'baz'::text))
+ Filter: (value > 10)
+ -> BitmapOr
+ -> Bitmap Index Scan on tpairs_key_idx
+ Index Cond: (key = 'foo'::text)
+ -> Bitmap Index Scan on tpairs_key_idx1
+ Index Cond: (key = 'bar'::text)
+ -> Bitmap Index Scan on tpairs_key_idx2
+ Index Cond: (key = 'baz'::text)
+(10 rows)
+
+-- Variant 3 (works only with this patch)
+EXPLAIN (COSTS OFF)
+SELECT value
+FROM tpairs
+WHERE
+value > 10 AND
+key = ANY('{foo,bar,baz}');
+ QUERY PLAN
+-------------------------------------------------------------------------------------
+ Bitmap Heap Scan on tpairs
+ Recheck Cond: ((key = 'foo'::text) OR (key = 'bar'::text) OR (key = 'baz'::text))
+ Filter: (value > 10)
+ -> BitmapOr
+ -> Bitmap Index Scan on tpairs_key_idx
+ Index Cond: (key = 'foo'::text)
+ -> Bitmap Index Scan on tpairs_key_idx1
+ Index Cond: (key = 'bar'::text)
+ -> Bitmap Index Scan on tpairs_key_idx2
+ Index Cond: (key = 'baz'::text)
+(10 rows)
+
+-- Clean up
+DROP TABLE tpairs;
diff --git a/src/test/regress/expected/saop_bitmap-2.out b/src/test/regress/expected/saop_bitmap-2.out
new file mode 100644
index 00000000000..0fa4054f407
--- /dev/null
+++ b/src/test/regress/expected/saop_bitmap-2.out
@@ -0,0 +1,101 @@
+-- Test ScalarArrayOpExpr for partial indexes
+-- Generate enough data that we can test the use of an index
+CREATE TABLE ipairs(
+ key INT,
+ value INT
+)
+WITH (autovacuum_enabled = false);
+-- Install the partial index on these values
+CREATE INDEX ON ipairs(key) WHERE key = 7;
+CREATE INDEX ON ipairs(key) WHERE key = 8;
+CREATE INDEX ON ipairs(key) WHERE key = 9;
+-- Dummy data to surround ours
+INSERT INTO ipairs(key, value)
+SELECT n, -n
+FROM generate_series(1, 10000) AS tmp(n);
+-- Update statistics
+VACUUM (ANALYZE) ipairs;
+-- Our specific test data to force the partial index
+EXPLAIN (ANALYZE, COSTS OFF, BUFFERS OFF, SUMMARY OFF, TIMING OFF)
+UPDATE ipairs
+SET value = 0
+WHERE key = ANY('{7,8,9}'::INT[]);
+ QUERY PLAN
+-----------------------------------------------------------------------------------
+ Update on ipairs (actual rows=0.00 loops=1)
+ -> Bitmap Heap Scan on ipairs (actual rows=3.00 loops=1)
+ Recheck Cond: ((key = 7) OR (key = 8) OR (key = 9))
+ Heap Blocks: exact=1
+ -> BitmapOr (actual rows=0.00 loops=1)
+ -> Bitmap Index Scan on ipairs_key_idx (actual rows=1.00 loops=1)
+ Index Cond: (key = 7)
+ Index Searches: 1
+ -> Bitmap Index Scan on ipairs_key_idx1 (actual rows=1.00 loops=1)
+ Index Cond: (key = 8)
+ Index Searches: 1
+ -> Bitmap Index Scan on ipairs_key_idx2 (actual rows=1.00 loops=1)
+ Index Cond: (key = 9)
+ Index Searches: 1
+(14 rows)
+
+-- We want ensure that an index use is always attempted - just for the test
+SET enable_seqscan=false;
+-- Variant 1 (works without this patch)
+EXPLAIN (COSTS OFF)
+SELECT value=0
+FROM ipairs
+WHERE
+ key = 7 OR
+ key = 8 OR
+ key = 9;
+ QUERY PLAN
+-------------------------------------------------------
+ Bitmap Heap Scan on ipairs
+ Recheck Cond: ((key = 7) OR (key = 8) OR (key = 9))
+ -> BitmapOr
+ -> Bitmap Index Scan on ipairs_key_idx
+ Index Cond: (key = 7)
+ -> Bitmap Index Scan on ipairs_key_idx1
+ Index Cond: (key = 8)
+ -> Bitmap Index Scan on ipairs_key_idx2
+ Index Cond: (key = 9)
+(9 rows)
+
+-- Variant 2 (works only with this patch)
+EXPLAIN (COSTS OFF)
+SELECT value=0
+FROM ipairs
+WHERE key IN (7, 8, 9);
+ QUERY PLAN
+-------------------------------------------------------
+ Bitmap Heap Scan on ipairs
+ Recheck Cond: ((key = 7) OR (key = 8) OR (key = 9))
+ -> BitmapOr
+ -> Bitmap Index Scan on ipairs_key_idx
+ Index Cond: (key = 7)
+ -> Bitmap Index Scan on ipairs_key_idx1
+ Index Cond: (key = 8)
+ -> Bitmap Index Scan on ipairs_key_idx2
+ Index Cond: (key = 9)
+(9 rows)
+
+-- Variant 3 (works only with this patch)
+EXPLAIN (COSTS OFF)
+SELECT value=0
+FROM ipairs
+WHERE key = ANY('{7,8,9}'::INT[]);
+ QUERY PLAN
+-------------------------------------------------------
+ Bitmap Heap Scan on ipairs
+ Recheck Cond: ((key = 7) OR (key = 8) OR (key = 9))
+ -> BitmapOr
+ -> Bitmap Index Scan on ipairs_key_idx
+ Index Cond: (key = 7)
+ -> Bitmap Index Scan on ipairs_key_idx1
+ Index Cond: (key = 8)
+ -> Bitmap Index Scan on ipairs_key_idx2
+ Index Cond: (key = 9)
+(9 rows)
+
+-- Clean up
+DROP TABLE ipairs;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index cc6d799bcea..e24859e3c74 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -102,7 +102,7 @@ test: publication subscription
# Another group of parallel tests
# select_views depends on create_view
# ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite saop_bitmap-1 saop_bitmap-2
# ----------
# Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/saop_bitmap-1.sql b/src/test/regress/sql/saop_bitmap-1.sql
new file mode 100644
index 00000000000..167c391e15d
--- /dev/null
+++ b/src/test/regress/sql/saop_bitmap-1.sql
@@ -0,0 +1,61 @@
+-- Test ScalarArrayOpExpr for partial indexes
+
+-- Generate enough data that we can test the use of an index
+CREATE TABLE tpairs(
+ key TEXT,
+ value int
+)
+WITH (autovacuum_enabled = false);
+-- Install the partial index on these values
+CREATE INDEX ON tpairs(key) WHERE key = 'foo';
+CREATE INDEX ON tpairs(key) WHERE key = 'bar';
+CREATE INDEX ON tpairs(key) WHERE key = 'baz';
+
+-- Dummy data to surround ours
+INSERT INTO tpairs(key, value)
+SELECT n::TEXT, n
+FROM generate_series(1, 10000) AS tmp(n);
+
+-- Our specific test data to force the partial index
+INSERT INTO tpairs(key, value)
+VALUES
+('foo', 25),
+('bar', 50),
+('baz', 75);
+
+-- Update statistics
+VACUUM (ANALYZE, FREEZE) tpairs;
+
+-- We want ensure that an index use is always attempted - just for the test
+SET enable_seqscan=false;
+
+-- Variant 1 (works without this patch)
+EXPLAIN (COSTS OFF)
+SELECT value
+FROM tpairs
+WHERE
+value > 10 AND
+(
+ key = 'foo' OR
+ key = 'bar' OR
+ key = 'baz'
+);
+
+-- Variant 2 (works only with this patch)
+EXPLAIN (COSTS OFF)
+SELECT value
+FROM tpairs
+WHERE
+value > 10 AND
+key IN ('foo', 'bar', 'baz');
+
+-- Variant 3 (works only with this patch)
+EXPLAIN (COSTS OFF)
+SELECT value
+FROM tpairs
+WHERE
+value > 10 AND
+key = ANY('{foo,bar,baz}');
+
+-- Clean up
+DROP TABLE tpairs;
diff --git a/src/test/regress/sql/saop_bitmap-2.sql b/src/test/regress/sql/saop_bitmap-2.sql
new file mode 100644
index 00000000000..12c9f7e3db9
--- /dev/null
+++ b/src/test/regress/sql/saop_bitmap-2.sql
@@ -0,0 +1,53 @@
+-- Test ScalarArrayOpExpr for partial indexes
+
+-- Generate enough data that we can test the use of an index
+CREATE TABLE ipairs(
+ key INT,
+ value INT
+)
+WITH (autovacuum_enabled = false);
+-- Install the partial index on these values
+CREATE INDEX ON ipairs(key) WHERE key = 7;
+CREATE INDEX ON ipairs(key) WHERE key = 8;
+CREATE INDEX ON ipairs(key) WHERE key = 9;
+
+-- Dummy data to surround ours
+INSERT INTO ipairs(key, value)
+SELECT n, -n
+FROM generate_series(1, 10000) AS tmp(n);
+
+-- Update statistics
+VACUUM (ANALYZE) ipairs;
+
+-- Our specific test data to force the partial index
+EXPLAIN (ANALYZE, COSTS OFF, BUFFERS OFF, SUMMARY OFF, TIMING OFF)
+UPDATE ipairs
+SET value = 0
+WHERE key = ANY('{7,8,9}'::INT[]);
+
+-- We want ensure that an index use is always attempted - just for the test
+SET enable_seqscan=false;
+
+-- Variant 1 (works without this patch)
+EXPLAIN (COSTS OFF)
+SELECT value=0
+FROM ipairs
+WHERE
+ key = 7 OR
+ key = 8 OR
+ key = 9;
+
+-- Variant 2 (works only with this patch)
+EXPLAIN (COSTS OFF)
+SELECT value=0
+FROM ipairs
+WHERE key IN (7, 8, 9);
+
+-- Variant 3 (works only with this patch)
+EXPLAIN (COSTS OFF)
+SELECT value=0
+FROM ipairs
+WHERE key = ANY('{7,8,9}'::INT[]);
+
+-- Clean up
+DROP TABLE ipairs;