v1-0001-Presort-ScalarArrayOp-arrays-during-planning.patch
application/x-patch
Filename: v1-0001-Presort-ScalarArrayOp-arrays-during-planning.patch
Type: application/x-patch
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v1-0001
Subject: Presort ScalarArrayOp arrays during planning.
| File | + | − |
|---|---|---|
| src/backend/access/nbtree/nbtpreprocesskeys.c | 102 | 5 |
| src/backend/executor/nodeIndexscan.c | 6 | 0 |
| src/backend/optimizer/plan/createplan.c | 19 | 0 |
| src/include/access/nbtree.h | 2 | 0 |
| src/include/access/skey.h | 4 | 1 |
| src/test/regress/expected/btree_index.out | 1 | 1 |
| src/test/regress/expected/create_index.out | 10 | 10 |
From ca69e66e705f92439349d8fed5a2ea93cbcbd470 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 31 Jul 2025 19:31:15 -0400
Subject: [PATCH v1] Presort ScalarArrayOp arrays during planning.
---
src/include/access/nbtree.h | 2 +
src/include/access/skey.h | 5 +-
src/backend/access/nbtree/nbtpreprocesskeys.c | 107 +++++++++++++++++-
src/backend/executor/nodeIndexscan.c | 6 +
src/backend/optimizer/plan/createplan.c | 19 ++++
src/test/regress/expected/btree_index.out | 2 +-
src/test/regress/expected/create_index.out | 20 ++--
7 files changed, 144 insertions(+), 17 deletions(-)
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index e709d2e0a..7f9b0373e 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1297,6 +1297,8 @@ extern void _bt_pendingfsm_finalize(Relation rel, BTVacState *vstate);
* prototypes for functions in nbtpreprocesskeys.c
*/
extern void _bt_preprocess_keys(IndexScanDesc scan);
+extern Datum _bt_presort_const_array(Datum scanvalue, Oid opno, Oid opfamily,
+ Oid collation, bool reverse);
/*
* prototypes for functions in nbtsearch.c
diff --git a/src/include/access/skey.h b/src/include/access/skey.h
index e650c2e7b..21fc13a1d 100644
--- a/src/include/access/skey.h
+++ b/src/include/access/skey.h
@@ -38,7 +38,9 @@
* "column op ANY(ARRAY[...])". This is signaled by the SK_SEARCHARRAY
* flag bit. The sk_argument is not a value of the operator's right-hand
* argument type, but rather an array of such values, and the per-element
- * comparisons are to be ORed together.
+ * comparisons are to be ORed together. Index AMs with native support for
+ * SAOP scans have their scan key marked SK_PRESORTED, indicating that the
+ * array has been sorted and deduplicated (and has had all NULLs removed).
*
* A ScanKey can also represent a condition "column IS NULL" or "column
* IS NOT NULL"; these cases are signaled by the SK_SEARCHNULL and
@@ -121,6 +123,7 @@ typedef ScanKeyData *ScanKey;
#define SK_SEARCHNULL 0x0040 /* scankey represents "col IS NULL" */
#define SK_SEARCHNOTNULL 0x0080 /* scankey represents "col IS NOT NULL" */
#define SK_ORDER_BY 0x0100 /* scankey is for ORDER BY op */
+#define SK_PRESORTED 0x0200 /* constant/presorted SAOP array */
/*
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 21c519cd1..4904f2aad 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -2017,10 +2017,18 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
* all btree operators are strict.
*/
num_nonnulls = 0;
- for (int j = 0; j < num_elems; j++)
+ if (!(cur->sk_flags & SK_PRESORTED))
{
- if (!elem_nulls[j])
- elem_values[num_nonnulls++] = elem_values[j];
+ for (int j = 0; j < num_elems; j++)
+ {
+ if (!elem_nulls[j])
+ elem_values[num_nonnulls++] = elem_values[j];
+ }
+ }
+ else
+ {
+ /* Planner removed NULLs for us using _bt_presort_const_array */
+ num_nonnulls = num_elems;
}
/* We could pfree(elem_nulls) now, but not worth the cycles */
@@ -2090,10 +2098,14 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys)
* sort in the same ordering used by the index column, so that the
* arrays can be advanced in lockstep with the scan's progress through
* the index's key space.
+ *
+ * When SK_PRESORTED is set, _bt_presort_const_array will have been
+ * called during planning to avoid repeated sorting across rescans.
*/
reverse = (indoption[cur->sk_attno - 1] & INDOPTION_DESC) != 0;
- num_elems = _bt_sort_array_elements(cur, sortprocp, reverse,
- elem_values, num_nonnulls);
+ if (!(cur->sk_flags & SK_PRESORTED))
+ num_elems = _bt_sort_array_elements(cur, sortprocp, reverse,
+ elem_values, num_nonnulls);
if (origarrayatt == cur->sk_attno)
{
@@ -2832,3 +2844,88 @@ _bt_compare_array_elements(const void *a, const void *b, void *arg)
INVERT_COMPARE_RESULT(compare);
return compare;
}
+
+/*
+ * _bt_presort_const_array() -- presort ScalarArrarOp array.
+ *
+ * Returns a sorted and deduplicated copy of caller's scanvalue array,
+ * enabling the array's input scan key to be marked SK_PRESORTED later on.
+ *
+ * Called by the planner whenever it encounters a ScalarArrayOp whose array
+ * arg is a constant. The array won't need to be sorted during preprocessing.
+ * This optimization saves many cycles with larger SAOP arrays that are reused
+ * across rescans.
+ */
+extern Datum
+_bt_presort_const_array(Datum scanvalue, Oid opno, Oid opfamily,
+ Oid collation, bool reverse)
+{
+ ArrayType *arrayval;
+ int16 elmlen;
+ bool elmbyval;
+ char elmalign;
+ int op_strategy;
+ Oid op_lefttype,
+ op_righttype;
+ Datum *elem_values;
+ bool *elem_nulls;
+ int num_elems,
+ num_nonnulls;
+ RegProcedure cmp_proc;
+ FmgrInfo sortproc;
+
+ /*
+ * Deconstruct the array into elements
+ */
+ arrayval = DatumGetArrayTypeP(scanvalue);
+ /* We could cache this data, but not clear it's worth it */
+ get_typlenbyvalalign(ARR_ELEMTYPE(arrayval), &elmlen, &elmbyval, &elmalign);
+ deconstruct_array(arrayval, ARR_ELEMTYPE(arrayval),
+ elmlen, elmbyval, elmalign,
+ &elem_values, &elem_nulls, &num_elems);
+
+ /*
+ * Compress out any null elements. We can ignore them since we assume all
+ * btree operators are strict.
+ */
+ num_nonnulls = 0;
+ for (int j = 0; j < num_elems; j++)
+ {
+ if (!elem_nulls[j])
+ elem_values[num_nonnulls++] = elem_values[j];
+ }
+
+ /* Look up the appropriate same-type comparison function in the opfamily */
+ get_op_opfamily_properties(opno, opfamily, false, &op_strategy,
+ &op_lefttype, &op_righttype);
+ cmp_proc = get_opfamily_proc(opfamily, op_righttype, op_righttype,
+ BTORDER_PROC);
+ if (!RegProcedureIsValid(cmp_proc))
+ elog(ERROR, "missing support function %d(%u,%u)",
+ BTORDER_PROC, op_righttype, op_righttype);
+
+ /* Set same-type ORDER proc for caller */
+ fmgr_info(cmp_proc, &sortproc);
+
+ /* Sort the non-null elements and eliminate any duplicates */
+ if (num_nonnulls > 1)
+ {
+ BTSortArrayContext cxt;
+
+ /* Sort the array elements */
+ cxt.sortproc = &sortproc;
+ cxt.collation = collation;
+ cxt.reverse = reverse;
+ qsort_arg(elem_values, num_nonnulls, sizeof(Datum),
+ _bt_compare_array_elements, &cxt);
+
+ /* Now scan the sorted elements and remove duplicates */
+ num_nonnulls = qunique_arg(elem_values, num_nonnulls, sizeof(Datum),
+ _bt_compare_array_elements, &cxt);
+ }
+
+ return PointerGetDatum(construct_array(elem_values, num_nonnulls,
+ ARR_ELEMTYPE(arrayval),
+ elmlen, elmbyval, elmalign));
+
+}
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index 7fcaa37fe..04ae671d7 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -1510,6 +1510,12 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
scanvalue = ((Const *) rightop)->constvalue;
if (((Const *) rightop)->constisnull)
flags |= SK_ISNULL;
+
+ /*
+ * Const SAOP arrays are presorted during planning (with
+ * indexes whose index AM is amsearcharray)
+ */
+ flags |= SK_PRESORTED;
}
else
{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index bfefc7dbe..4757cd6f2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -18,6 +18,7 @@
#include <math.h>
+#include "access/nbtree.h"
#include "access/sysattr.h"
#include "catalog/pg_class.h"
#include "foreign/fdwapi.h"
@@ -5280,11 +5281,29 @@ fix_indexqual_clause(PlannerInfo *root, IndexOptInfo *index, int indexcol,
else if (IsA(clause, ScalarArrayOpExpr))
{
ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
+ Expr *arrayarg;
+ Const *arrayargconst = NULL;
/* Replace the indexkey expression with an index Var. */
linitial(saop->args) = fix_indexqual_operand(linitial(saop->args),
index,
indexcol);
+
+ /*
+ * amsearcharray index AMs generally work with a sorted array at
+ * runtime. If a SAOP array argument is a Const, we presort here.
+ * This avoids repeating sorting/deduplicating work across rescans.
+ */
+ Assert(list_length(saop->args) == 2);
+ arrayarg = (Expr *) lsecond(saop->args);
+ if (index->amsearcharray && IsA(arrayarg, Const))
+ arrayargconst = (Const *) arrayarg;
+ if (arrayargconst && !arrayargconst->constisnull)
+ arrayargconst->constvalue =
+ _bt_presort_const_array(arrayargconst->constvalue,
+ saop->opno, index->opfamily[indexcol],
+ index->indexcollations[indexcol],
+ index->reverse_sort[indexcol]);
}
else if (IsA(clause, NullTest))
{
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 21dc9b578..d78adcdcf 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -296,7 +296,7 @@ ORDER BY proname, proargtypes, pronamespace;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Index Only Scan using pg_proc_proname_args_nsp_index on pg_proc
- Index Cond: ((ROW(proname, proargtypes) > ROW('abs'::name, NULL::oidvector)) AND (proname = 'zzzzzz'::name) AND (proargtypes = ANY ('{"26 23",5077}'::oidvector[])) AND (pronamespace = ANY ('{1,2,3}'::oid[])))
+ Index Cond: ((ROW(proname, proargtypes) > ROW('abs'::name, NULL::oidvector)) AND (proname = 'zzzzzz'::name) AND (proargtypes = ANY ('{5077,"26 23"}'::oidvector[])) AND (pronamespace = ANY ('{1,2,3}'::oid[])))
(2 rows)
SELECT proname, proargtypes, pronamespace
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index 98e68e972..055b8a064 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2287,7 +2287,7 @@ ORDER BY unique1;
QUERY PLAN
-------------------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
- Index Cond: (unique1 = ANY ('{1,42,7}'::integer[]))
+ Index Cond: (unique1 = ANY ('{1,7,42}'::integer[]))
(2 rows)
SELECT unique1 FROM tenk1
@@ -2367,7 +2367,7 @@ SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = ANY('{7, 8,
QUERY PLAN
----------------------------------------------------------------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
- Index Cond: ((unique1 = ANY ('{1,42,7}'::integer[])) AND (unique1 = ANY ('{7,8,9}'::integer[])))
+ Index Cond: ((unique1 = ANY ('{1,7,42}'::integer[])) AND (unique1 = ANY ('{7,8,9}'::integer[])))
(2 rows)
SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = ANY('{7, 8, 9}');
@@ -2404,10 +2404,10 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY(NULL);
explain (costs off)
SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
- QUERY PLAN
----------------------------------------------------------------
+ QUERY PLAN
+-------------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
- Index Cond: (unique1 = ANY ('{NULL,NULL,NULL}'::integer[]))
+ Index Cond: (unique1 = ANY ('{}'::integer[]))
(2 rows)
SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}');
@@ -2433,7 +2433,7 @@ SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
QUERY PLAN
---------------------------------------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
- Index Cond: ((unique1 = ANY ('{1,42,7}'::integer[])) AND (unique1 = 1))
+ Index Cond: ((unique1 = ANY ('{1,7,42}'::integer[])) AND (unique1 = 1))
(2 rows)
SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 1;
@@ -2447,7 +2447,7 @@ SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 12345;
QUERY PLAN
-------------------------------------------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
- Index Cond: ((unique1 = ANY ('{1,42,7}'::integer[])) AND (unique1 = 12345))
+ Index Cond: ((unique1 = ANY ('{1,7,42}'::integer[])) AND (unique1 = 12345))
(2 rows)
SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 = 12345;
@@ -2460,7 +2460,7 @@ SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 >= 42;
QUERY PLAN
-----------------------------------------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
- Index Cond: ((unique1 = ANY ('{1,42,7}'::integer[])) AND (unique1 >= 42))
+ Index Cond: ((unique1 = ANY ('{1,7,42}'::integer[])) AND (unique1 >= 42))
(2 rows)
SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 >= 42;
@@ -2474,7 +2474,7 @@ SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 > 42;
QUERY PLAN
----------------------------------------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
- Index Cond: ((unique1 = ANY ('{1,42,7}'::integer[])) AND (unique1 > 42))
+ Index Cond: ((unique1 = ANY ('{1,7,42}'::integer[])) AND (unique1 > 42))
(2 rows)
SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 > 42;
@@ -2530,7 +2530,7 @@ SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 < (-1)::bigint
QUERY PLAN
--------------------------------------------------------------------------------------
Index Only Scan using tenk1_unique1 on tenk1
- Index Cond: ((unique1 = ANY ('{1,42,7}'::integer[])) AND (unique1 < '-1'::bigint))
+ Index Cond: ((unique1 = ANY ('{1,7,42}'::integer[])) AND (unique1 < '-1'::bigint))
(2 rows)
SELECT unique1 FROM tenk1 WHERE unique1 IN (1, 42, 7) and unique1 < (-1)::bigint;
--
2.50.0