Re: Partition pruning on parameters grouped into an array does not prune properly - Mailing list pgsql-hackers

From Amit Langote
Subject Re: Partition pruning on parameters grouped into an array does not prune properly
Date
Msg-id CA+HiwqEuJV2-UXtU-kL7JwABjAh-DN7y9F7YCwqO_4BA_U07NQ@mail.gmail.com
Whole thread Raw
In response to Re: Partition pruning on parameters grouped into an array does not prune properly  (David Rowley <dgrowleyml@gmail.com>)
List pgsql-hackers
On Thu, Mar 27, 2025 at 9:59 AM David Rowley <dgrowleyml@gmail.com> wrote:
> On Thu, 27 Mar 2025 at 04:19, Andrei Lepikhov <lepihov@gmail.com> wrote:
> > But if we partition on HASH(x,y) it is not working (see
> > incorrect-pruning-example.sql):
> >
> > PREPARE test2 (int,int) AS
> >   SELECT 1 FROM array_prune
> >   WHERE id1 = ANY(ARRAY[$1]) AND id2 = ANY(ARRAY[$2]);
> > EXPLAIN (COSTS OFF) EXECUTE test2(1,-1);
> >
> >   Append
> >     ->  Seq Scan on array_prune_t0 array_prune_1
> >           Filter: ((id1 = ANY (ARRAY[$1])) AND (id2 = ANY (ARRAY[$2])))
> >     ->  Seq Scan on array_prune_t1 array_prune_2
> >           Filter: ((id1 = ANY (ARRAY[$1])) AND (id2 = ANY (ARRAY[$2])))
>
> It is a bug.  This is down to how match_clause_to_partition_key()
> handles ScalarArrayOpExpr.  To save some complexity in the handling of
> ScalarArrayOpExpr, these get transformed into OpExprs, one for each
> item in the ScalarArrayOpExpr.  Look for the call to make_opclause()
> in match_clause_to_partition_key(). Just a few lines down, you see
> that we recursively call gen_partprune_steps_internal() to pass down
> the OpExprs that we just generated.  The problem is that the recursive
> call only contains the OpExprs generated for one of the
> ScalarArrayOpExpr, gen_prune_steps_from_opexps() requires equality
> quals (or at least an key IS NULL qual) for all partitioned keys for
> hash partitioning, otherwise it'll bail out on the following:
>
> if (part_scheme->strategy == PARTITION_STRATEGY_HASH &&
> clauselist == NIL && !bms_is_member(i, nullkeys))
>     return NIL;
>
> I wonder if we need to redesign this to not do that recursive
> processing and instead have it so match_clause_to_partition_key() can
> generate multiple PartClauseInfos. If we've matched to the
> ScalarArrayOpExpr then I think each generated PartClauseInfo should
> have the same PartClauseMatchStatus. That would also get rid of the
> (kinda silly) overhead we have of having to match the
> ScalarArrayOpExpr to the partition key, then generating OpExprs and
> having to match those again, even though we know they will match.

Hmm, how would the multiple PartClauseInfos that would be generated in
this case -- presumably one per element in the ScalarArrayOpExpr -- be
interpreted by gen_partprune_steps_internal()? As things stand, that
code assumes multiple clauses for a given key are to be combined using
logical AND, which doesn’t line up with how we handle
ScalarArrayOpExpr with ANY. In that case (useOr = true), we expand the
clause into individual OpExprs for each element, wrap them in an
OR_EXPR, and pass that down recursively to
gen_partprune_steps_internal(), which we know works correctly for
single partition key case.  So unless we also rethink how pruning step
generation in the caller (gen_partprune_steps_internal()) interprets
multiple PartClauseInfos for the same key, just returning more of them
from match_clause_to_partition_key() wouldn’t be enough. We’d need to
restructure the caller to combine them in a way that reflects the
intended cross-product of values across partition keys.

For example, consider:

key1 = ANY(ARRAY[1, 2]) AND key2 = ANY(ARRAY[3, 4])

which expands to:

(key1 = 1 OR key1 = 2) AND (key2 = 3 OR key2 = 4)

and then to DNF:

(key1 = 1 AND key2 = 3)
OR (key1 = 1 AND key2 = 4)
OR (key1 = 2 AND key2 = 3)
OR (key1 = 2 AND key2 = 4)

To support pruning in this case, we’d need to extend
gen_partprune_steps_internal() to construct value combinations across
independently matched keys. The way that could work is for each (key1,
key2) pair in the cross-product, we’d generate a PartitionPruneStepOp,
and combine their results using an OR-mode PartitionPruneStepCombine
step.

As for whether this qualifies as a bug, I’d lean toward “no” -- at
least in the sense that it doesn’t produce incorrect results. We’ve
historically treated unsupported pruning cases like this as missed
optimization opportunities rather than planner correctness issues.
That would argue against backpatching a fix, even if we do decide to
improve the pruning logic in this area and for other cases going
forward.

--
Thanks, Amit Langote



pgsql-hackers by date:

Previous
From: Jacob Champion
Date:
Subject: Re: Add missing PGDLLIMPORT markings
Next
From: Jacob Champion
Date:
Subject: Re: [PoC] Federated Authn/z with OAUTHBEARER