From aa163906db6c872ea4a7ca406a2b52928439f6ab Mon Sep 17 00:00:00 2001
From: Jim Vanns <james.vanns@gmail.com>
Date: Mon, 8 Jun 2026 08:02:42 +0100
Subject: [PATCH 4/5] [PATCH 4/5] Fix costing and planner overhead in SAOP
 partial index optimization

In generate_bitmap_saop_paths() do NOT prune based on !clauseset.nonempty
here. If predicate_implied_by() passed above, an empty clauseset
simply results in a full scan of the partial index, which is perfectly
valid and desired.

The initial implementation of generate_bitmap_saop_paths failed to
match other base query restriction clauses (e.g., range boundaries on
the indexed key) against candidate partial indexes during the array
element decomposition phase. This resulted in an empty clauseset being
passed to build_index_paths(), forcing the planner to assume a full scan
of each partial index. Consequently, the overstated cost estimates
caused the planner to reject the valid BitmapOr paths for large arrays.

Attempting to match these base restrictions inside the per-element
loop introduced an unacceptable O(N*M) complexity, spiking planner
overhead significantly when evaluating large arrays against multiple
candidate partial indexes.

Fix this by hoisting the evaluation of non-SAOP restrictions out of the
inner element processing loop. We pre-calculate an array of base
IndexClauseSet structs for all suitable candidate indexes using standard
palloc and explicit MemSet to align with indxpath.c styling. Inside the
per-element loop, we then perform a fast stack copy via memcpy() of the
pre-calculated clause boundaries before appending the synthesized scalar
element clause.

This brings the total planning complexity back down to nearer O(M(R+N)),
where N = array elements, M = indexes and R = non-SAOP restrictions,
while properly factoring in all restriction clauses, ensuring the optimizer
accurately costs and selects the optimized ScalarArrayOpExpr paths.

diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index b980854a582..9fc7ae31393 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -1920,6 +1920,35 @@ generate_bitmap_saop_paths(PlannerInfo *root, RelOptInfo *rel,
                           &elem_nulls,
                           &nelems);
 
+        /*
+         * Create an array to hold the base matched clauses for each index.
+         * We match all query restrictions (except the SAOP itself) against
+         * our candidate indexes just once.
+         */
+        IndexClauseSet *base_clausesets =
+            (IndexClauseSet *) palloc0(list_length(indexes) * sizeof(IndexClauseSet));
+
+        {
+            int idx_pos = -1;
+            while ((idx_pos = bms_next_member(suitable_indexes, idx_pos)) >= 0)
+            {
+                IndexOptInfo *index = list_nth(indexes, idx_pos);
+                ListCell *clc;
+                
+                foreach(clc, clauses)
+                {
+                    RestrictInfo *other_rinfo = lfirst_node(RestrictInfo, clc);
+                    /* Skip the original SAOP clause we are actively decomposing */
+                    if (other_rinfo != rinfo) {
+                        match_clause_to_index(root,
+                                              other_rinfo,
+                                              index,
+                                              &base_clausesets[idx_pos]);
+                    }
+                }
+            }
+        }
+
         /*
          * For each IN element, build ALL possible index paths,
          * not just the first one (avoid greedy choice).
@@ -1976,24 +2005,12 @@ generate_bitmap_saop_paths(PlannerInfo *root, RelOptInfo *rel,
                                           false))
                     continue;
 
-                MemSet(&clauseset, 0, sizeof(clauseset));
+                memcpy(&clauseset, &base_clausesets[index_pos], sizeof(IndexClauseSet));
 
                 match_clause_to_index(root,
                                       new_rinfo,
                                       index,
                                       &clauseset);
-
-                /*
-                 * Structural mismatch → prune permanently.
-                 */
-                if (!clauseset.nonempty)
-                {
-                    to_remove =
-                        bms_add_member(to_remove,
-                                       index_pos);
-                    continue;
-                }
-
                 indexpaths =
                     build_index_paths(root,
                                       rel,
@@ -2056,6 +2073,7 @@ generate_bitmap_saop_paths(PlannerInfo *root, RelOptInfo *rel,
             }
         }
 
+        pfree(base_clausesets);
         bms_free(suitable_indexes);
 
         if (per_saop_paths != NIL)
-- 
2.43.0

