pg.ddx.io  pgsql-bugs@postgresql.org mailing list archive  
help / color / mirror / Atom feed
BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
12+ messages / 6 participants
[nested] [flat]

* BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
@ 2024-02-15 09:00 PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  0 siblings, 1 reply; 12+ messages in thread

From: PG Bug reporting form @ 2024-02-15 09:00 UTC (permalink / raw)
  To: pgsql-bugs@lists.postgresql.org; +Cc: exclusion@gmail.com

The following bug has been logged on the website:

Bug reference:      18344
Logged by:          Alexander Lakhin
Email address:      exclusion@gmail.com
PostgreSQL version: 16.2
Operating system:   Ubuntu 22.04
Description:        

The following query:
CREATE TABLE t (b bool, i int) PARTITION BY RANGE (b, i);
CREATE TABLE tp PARTITION OF t FOR VALUES FROM (false, 0) TO (false, 1);
SELECT * FROM t WHERE b IS NOT true;

fails with ERROR:  invalid strategy number 0.

Reproduced on REL_12_STABLE .. master.
The first bad commit for this anomaly is e0693faf7.



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
@ 2024-02-15 16:28 ` Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 02:56   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  0 siblings, 2 replies; 12+ messages in thread

From: Tom Lane @ 2024-02-15 16:28 UTC (permalink / raw)
  To: exclusion@gmail.com; +Cc: pgsql-bugs@lists.postgresql.org, David Rowley <dgrowleyml@gmail.com>

PG Bug reporting form <noreply@postgresql.org> writes:
> The following query:
> CREATE TABLE t (b bool, i int) PARTITION BY RANGE (b, i);
> CREATE TABLE tp PARTITION OF t FOR VALUES FROM (false, 0) TO (false, 1);
> SELECT * FROM t WHERE b IS NOT true;
> fails with ERROR:  invalid strategy number 0.
> Reproduced on REL_12_STABLE .. master.
> The first bad commit for this anomaly is e0693faf7.

What seems to be happening is that gen_prune_step_op is getting
op_is_ne = true and doing this:

	/*
	 * For clauses that contain an <> operator, set opstrategy to
	 * InvalidStrategy to signal get_matching_list_bounds to do the right
	 * thing.
	 */
	opstep->opstrategy = op_is_ne ? InvalidStrategy : opstrategy;

but then we're failing in get_matching_range_bounds, ie somebody
taught get_matching_list_bounds to do the right thing but not
any of the other code paths.

I'm also wondering how we got there in the first place.  It looks like
match_boolean_partition_clause thinks it can translate "b IS NOT true"
to "b <> true", which is flat wrong --- it gives the wrong result for
null.

			regards, tom lane





^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
@ 2024-02-16 02:56   ` David Rowley <dgrowleyml@gmail.com>
  1 sibling, 0 replies; 12+ messages in thread

From: David Rowley @ 2024-02-16 02:56 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: exclusion@gmail.com; pgsql-bugs@lists.postgresql.org

On Fri, 16 Feb 2024 at 05:28, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> What seems to be happening is that gen_prune_step_op is getting
> op_is_ne = true and doing this:
>
>         /*
>          * For clauses that contain an <> operator, set opstrategy to
>          * InvalidStrategy to signal get_matching_list_bounds to do the right
>          * thing.
>          */
>         opstep->opstrategy = op_is_ne ? InvalidStrategy : opstrategy;
>
> but then we're failing in get_matching_range_bounds, ie somebody
> taught get_matching_list_bounds to do the right thing but not
> any of the other code paths.

hmm, yeah. I'm just trying to wrap my head around if this can even
work for RANGE partitioned tables.

> I'm also wondering how we got there in the first place.  It looks like
> match_boolean_partition_clause thinks it can translate "b IS NOT true"
> to "b <> true", which is flat wrong --- it gives the wrong result for
> null.

Thought I'd fixed that in e0693faf7, but looks like I only tested it
with DEFAULT partitions, not NULL partitions. A fairly simple fix for
that part:

                /* Always include the default partition if any. */
                result->scan_default = partition_bound_has_default(boundinfo);

+               /* Likewise for the NULL partition, if any */
+               result->scan_null = partition_bound_accepts_nulls(boundinfo);

I'll look at the RANGE <> bool stuff.

David





^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
@ 2024-02-16 12:32   ` David Rowley <dgrowleyml@gmail.com>
  2024-02-18 12:53     ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  1 sibling, 1 reply; 12+ messages in thread

From: David Rowley @ 2024-02-16 12:32 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: exclusion@gmail.com; pgsql-bugs@lists.postgresql.org

On Fri, 16 Feb 2024 at 05:28, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> What seems to be happening is that gen_prune_step_op is getting
> op_is_ne = true and doing this:
>
>         /*
>          * For clauses that contain an <> operator, set opstrategy to
>          * InvalidStrategy to signal get_matching_list_bounds to do the right
>          * thing.
>          */
>         opstep->opstrategy = op_is_ne ? InvalidStrategy : opstrategy;
>
> but then we're failing in get_matching_range_bounds, ie somebody
> taught get_matching_list_bounds to do the right thing but not
> any of the other code paths.

Yeah, prior to e0693faf7, we always did:

       partclause->op_is_ne = false;

so the code you quoted would always use the equality opstrategy,
therefore wouldn't hit the "if (opstrategy == InvalidStrategy)" block
in get_matching_list_bounds().

The old code wrongly assumed "bool IS NOT true" was the same as "bool
IS false" and vice-versa.  I had thought I could fix this by making it
use the same code that we do for cases like int4col <> 123, but:

a) only get_matching_list_bounds() knows how to handle those, not the
equivalent hash and range versions and;
b) bool is not true matches NULLs, but int4col <> 123 does not.

So, I'm a little unsure if we should try and make bool IS NOT clauses
prune partitions at all. It was a bug in the original code that
thought we could do that, and it looks like I didn't do a good job of
fixing that.

I see three options:

1) Make get_matching_list_bounds() work for bool IS NOT clauses by
properly including the NULL partition when handling a bool IS NOT
clause.
2) Just don't generate a pruning step for bool IS NOT clauses.
3) Just always include the NULL partition in
get_matching_list_bounds's "if (opstrategy == InvalidStrategy)" block.

I don't quite see how to do #1 as we don't have an easy way to tell if
we're handling bool IS NOT clauses inside get_matching_list_bounds().
Maybe we could check the comparison function is btboolcmp. That's
kinda cruddy. We don't have oid_symbol for pg_proc.dat as we do in
pg_operator.dat, so there's no #define for the pg_proc entry's Oid.

If we do #2, I'm concerned we'll regress someone's workload by
including the other bool partition.  Likewise, for #3, we'll include
the NULL partition for non-boolean cases, which could cause someone
problems.

The attached does #3 plus disables "bool IS NOT" pruning for RANGE and
HASH to avoid the reported "invalid strategy number 0." error.

Maybe we could do #1 by checking for partsupfunc.fn_oid == 1693 in the
back branches and come up with a cleaner way in master by adding a new
field to PartitionPruneStepOp.

Keen to get feedback on this one. Also included Amit and Álvaro as
they might have another idea.

I also noticed that the following code returns PARTCLAUSE_NOMATCH:

if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
    return PARTCLAUSE_NOMATCH;

I think that should return PARTCLAUSE_UNSUPPORTED. But it's really
only an inefficiency rather than a bug, I think.

David
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 6b635e8ad1..2b582a0e72 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1810,6 +1810,13 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		PartClauseInfo *partclause;
 
+		/*
+		 * Boolean exprs in the form of IS NOT false, IS NOT true are only supported
+		 * for LIST partitioned tables.
+		 */
+		if (noteq && part_scheme->strategy != PARTITION_STRATEGY_LIST)
+			return PARTCLAUSE_UNSUPPORTED;
+
 		partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo));
 		partclause->keyno = partkeyidx;
 		/* Do pruning with the Boolean equality operator. */
@@ -2757,6 +2764,15 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		/* Always include the default partition if any. */
 		result->scan_default = partition_bound_has_default(boundinfo);
 
+		/*
+		 * XXX this line will cause is to match to the NULL partition for
+		 * non-boolean cases too.  That's a problem as int4 <> const will
+		 * not match NULLs, so we'll scan the NULL partition needlessly for
+		 * those cases.  We'd need a way to check if we're handling bools
+		 * here.
+		 */
+		result->scan_null = partition_bound_accepts_nulls(boundinfo);
+
 		return result;
 	}
 


Attachments:

  [text/plain] prune_on_bool_clause_experiements.patch (1.3K, ../../CAApHDvqriy8mPOFJ_Bd66YGXJ4+XULpv-4YdB+ePdCQFztyisA@mail.gmail.com/2-prune_on_bool_clause_experiements.patch)
  download | inline diff:
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 6b635e8ad1..2b582a0e72 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1810,6 +1810,13 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		PartClauseInfo *partclause;
 
+		/*
+		 * Boolean exprs in the form of IS NOT false, IS NOT true are only supported
+		 * for LIST partitioned tables.
+		 */
+		if (noteq && part_scheme->strategy != PARTITION_STRATEGY_LIST)
+			return PARTCLAUSE_UNSUPPORTED;
+
 		partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo));
 		partclause->keyno = partkeyidx;
 		/* Do pruning with the Boolean equality operator. */
@@ -2757,6 +2764,15 @@ get_matching_list_bounds(PartitionPruneContext *context,
 		/* Always include the default partition if any. */
 		result->scan_default = partition_bound_has_default(boundinfo);
 
+		/*
+		 * XXX this line will cause is to match to the NULL partition for
+		 * non-boolean cases too.  That's a problem as int4 <> const will
+		 * not match NULLs, so we'll scan the NULL partition needlessly for
+		 * those cases.  We'd need a way to check if we're handling bools
+		 * here.
+		 */
+		result->scan_null = partition_bound_accepts_nulls(boundinfo);
+
 		return result;
 	}
 


^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
@ 2024-02-18 12:53     ` David Rowley <dgrowleyml@gmail.com>
  2024-02-18 16:24       ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  0 siblings, 1 reply; 12+ messages in thread

From: David Rowley @ 2024-02-18 12:53 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: exclusion@gmail.com; pgsql-bugs@lists.postgresql.org

On Sat, 17 Feb 2024 at 01:32, David Rowley <dgrowleyml@gmail.com> wrote:
> I see three options:
>
> 1) Make get_matching_list_bounds() work for bool IS NOT clauses by
> properly including the NULL partition when handling a bool IS NOT
> clause.
> 2) Just don't generate a pruning step for bool IS NOT clauses.
> 3) Just always include the NULL partition in
> get_matching_list_bounds's "if (opstrategy == InvalidStrategy)" block.

It turns out there's a 4th, and much better option that allows this
just to work without any weirdness.

The method used in partprune.c to handle "partkey IN ('const1',
'const2')" is to transform that into "partkey = 'const1' OR partkey =
'const2'". Whenever we see a ScalarArrayOpExpr with consts, we just
form such an OR clause and recursively generate pruning steps for the
OR clause. That'll end up creating two pruning steps and combining
them with a PARTPRUNE_COMBINE_UNION.

We can do the same for BooleanTests. Given a clause such as: "partkey
IS NOT false", we can just generate the clause "partkey IS true OR
partkey IS NULL" and recursively generate steps for that.

I've attached a draft patch.  I'll work on this more after I sleep.

I'm tempted to go a bit further in master only and add support for
bool IS NOT UNKNOWN and bool IS UNKNOWN using the same method.

David
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 6b635e8ad1..94cfe5167b 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1810,11 +1810,60 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		PartClauseInfo *partclause;
 
+		/*
+		 * For bool tests in the form of partkey IS NOT true and IS NOT false,
+		 * we invert these clauses.  Effectively, "partkey IS NOT true" becomes
+		 * "partkey IS false OR partkey IS NULL".  We do this by building an OR
+		 * BoolExpr and forming a clause just like that and punt it off to
+		 * gen_partprune_steps_internal() to generate pruning steps.
+		 */
+		if (noteq)
+		{
+			List *bool_clauses;
+			BooleanTest *new_booltest = copyObject(clause);
+			NullTest *nulltest;
+
+			/* We expect 'noteq' to only be set to true for BooleanTests */
+			Assert(IsA(clause, BooleanTest));
+
+			/* reverse the bool test */
+			if (new_booltest->booltesttype == IS_NOT_TRUE)
+				new_booltest->booltesttype = IS_FALSE;
+			else if (new_booltest->booltesttype == IS_NOT_FALSE)
+				new_booltest->booltesttype = IS_TRUE;
+			else
+			{
+				/*
+				 * We only expect match_boolean_partition_clause to match for
+				 * IS_NOT_TRUE and IS_NOT_FALSE.  IS_NOT_UNKNOWN is not
+				 * supported.
+				 */
+				Assert(false);
+			}
+
+			nulltest = makeNode(NullTest);
+			nulltest->arg = copyObject(partkey);
+			nulltest->nulltesttype = IS_NULL;
+			nulltest->argisrow = false;
+			nulltest->location = -1;
+
+			bool_clauses = list_make1(makeBoolExpr(OR_EXPR, list_make2(new_booltest, nulltest), -1));
+
+			/* Finally, generate steps */
+			*clause_steps = gen_partprune_steps_internal(context, bool_clauses);
+
+			if (context->contradictory)
+				return PARTCLAUSE_MATCH_CONTRADICT; /* shouldn't happen */
+			else if (*clause_steps == NIL)
+				return PARTCLAUSE_UNSUPPORTED; /* step generation failed */
+			return PARTCLAUSE_MATCH_STEPS;
+		}
+
 		partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo));
 		partclause->keyno = partkeyidx;
 		/* Do pruning with the Boolean equality operator. */
 		partclause->opno = BooleanEqualOperator;
-		partclause->op_is_ne = noteq;
+		partclause->op_is_ne = false;
 		partclause->expr = expr;
 		/* We know that expr is of Boolean type. */
 		partclause->cmpfn = part_scheme->partsupfunc[partkeyidx].fn_oid;
@@ -2358,7 +2407,7 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
  * For LIST and RANGE partitioned tables, callers must ensure that
  * step_nullkeys is NULL, and that prefix contains at least one clause for
  * each of the partition keys prior to the key that 'step_lastexpr' and
- * 'step_lastcmpfn'belong to.
+ * 'step_lastcmpfn' belong to.
  *
  * For HASH partitioned tables, callers must ensure that 'prefix' contains at
  * least one clause for each of the partition keys apart from the final key


Attachments:

  [text/plain] fix_partprune_BooleanTests_draft.patch (2.9K, ../../CAApHDvoFbt0fne1C_prK5j0MvxHccJD7NxUY0UDaTtAZuEyxrg@mail.gmail.com/2-fix_partprune_BooleanTests_draft.patch)
  download | inline diff:
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 6b635e8ad1..94cfe5167b 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1810,11 +1810,60 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		PartClauseInfo *partclause;
 
+		/*
+		 * For bool tests in the form of partkey IS NOT true and IS NOT false,
+		 * we invert these clauses.  Effectively, "partkey IS NOT true" becomes
+		 * "partkey IS false OR partkey IS NULL".  We do this by building an OR
+		 * BoolExpr and forming a clause just like that and punt it off to
+		 * gen_partprune_steps_internal() to generate pruning steps.
+		 */
+		if (noteq)
+		{
+			List *bool_clauses;
+			BooleanTest *new_booltest = copyObject(clause);
+			NullTest *nulltest;
+
+			/* We expect 'noteq' to only be set to true for BooleanTests */
+			Assert(IsA(clause, BooleanTest));
+
+			/* reverse the bool test */
+			if (new_booltest->booltesttype == IS_NOT_TRUE)
+				new_booltest->booltesttype = IS_FALSE;
+			else if (new_booltest->booltesttype == IS_NOT_FALSE)
+				new_booltest->booltesttype = IS_TRUE;
+			else
+			{
+				/*
+				 * We only expect match_boolean_partition_clause to match for
+				 * IS_NOT_TRUE and IS_NOT_FALSE.  IS_NOT_UNKNOWN is not
+				 * supported.
+				 */
+				Assert(false);
+			}
+
+			nulltest = makeNode(NullTest);
+			nulltest->arg = copyObject(partkey);
+			nulltest->nulltesttype = IS_NULL;
+			nulltest->argisrow = false;
+			nulltest->location = -1;
+
+			bool_clauses = list_make1(makeBoolExpr(OR_EXPR, list_make2(new_booltest, nulltest), -1));
+
+			/* Finally, generate steps */
+			*clause_steps = gen_partprune_steps_internal(context, bool_clauses);
+
+			if (context->contradictory)
+				return PARTCLAUSE_MATCH_CONTRADICT; /* shouldn't happen */
+			else if (*clause_steps == NIL)
+				return PARTCLAUSE_UNSUPPORTED; /* step generation failed */
+			return PARTCLAUSE_MATCH_STEPS;
+		}
+
 		partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo));
 		partclause->keyno = partkeyidx;
 		/* Do pruning with the Boolean equality operator. */
 		partclause->opno = BooleanEqualOperator;
-		partclause->op_is_ne = noteq;
+		partclause->op_is_ne = false;
 		partclause->expr = expr;
 		/* We know that expr is of Boolean type. */
 		partclause->cmpfn = part_scheme->partsupfunc[partkeyidx].fn_oid;
@@ -2358,7 +2407,7 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
  * For LIST and RANGE partitioned tables, callers must ensure that
  * step_nullkeys is NULL, and that prefix contains at least one clause for
  * each of the partition keys prior to the key that 'step_lastexpr' and
- * 'step_lastcmpfn'belong to.
+ * 'step_lastcmpfn' belong to.
  *
  * For HASH partitioned tables, callers must ensure that 'prefix' contains at
  * least one clause for each of the partition keys apart from the final key


^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 12:53     ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
@ 2024-02-18 16:24       ` Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-18 23:49         ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  0 siblings, 1 reply; 12+ messages in thread

From: Tom Lane @ 2024-02-18 16:24 UTC (permalink / raw)
  To: David Rowley <dgrowleyml@gmail.com>; +Cc: Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; exclusion@gmail.com; pgsql-bugs@lists.postgresql.org

David Rowley <dgrowleyml@gmail.com> writes:
> We can do the same for BooleanTests. Given a clause such as: "partkey
> IS NOT false", we can just generate the clause "partkey IS true OR
> partkey IS NULL" and recursively generate steps for that.

+1 ... sounds clean and clearly correct.

> I'm tempted to go a bit further in master only and add support for
> bool IS NOT UNKNOWN and bool IS UNKNOWN using the same method.

These are the same as IS NOT NULL and IS NULL, so I don't see the
need for an OR?

			regards, tom lane





^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 12:53     ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 16:24       ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
@ 2024-02-18 23:49         ` David Rowley <dgrowleyml@gmail.com>
  2024-02-19 08:21           ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tender Wang <tndrwang@gmail.com>
  2024-02-19 09:45           ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Amit Langote <amitlangote09@gmail.com>
  2024-02-20 03:00           ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Alexander Lakhin <exclusion@gmail.com>
  0 siblings, 3 replies; 12+ messages in thread

From: David Rowley @ 2024-02-18 23:49 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; exclusion@gmail.com; pgsql-bugs@lists.postgresql.org

On Mon, 19 Feb 2024 at 05:25, Tom Lane <tgl@sss.pgh.pa.us> wrote:
>
> David Rowley <dgrowleyml@gmail.com> writes:
> > We can do the same for BooleanTests. Given a clause such as: "partkey
> > IS NOT false", we can just generate the clause "partkey IS true OR
> > partkey IS NULL" and recursively generate steps for that.
>
> +1 ... sounds clean and clearly correct.

Here's a more complete patch for this.  I included some tests for LIST
and RANGE partitioned tables. I did manual testing for HASH, and was
on the fence about covering that too.

I did try the following using the table from the tests:

select * from boolrangep where a is not true and not b and c = 25 and
a is not null;

When will be effectively transformed into:

select * from boolrangep where (a is false or a is null) and not b and
c = 25 and a is not null;

It seems that's unable to prune the NULL partition but that mostly
seems to be due to a limitation of the current design. I'm not sure
it's worth going to any additional trouble to make that work.  It
seems a bit unlikely, especially so given how long the BooleanTest
pruning stuff was broken for before anyone noticed.

> > I'm tempted to go a bit further in master only and add support for
> > bool IS NOT UNKNOWN and bool IS UNKNOWN using the same method.
>
> These are the same as IS NOT NULL and IS NULL, so I don't see the
> need for an OR?

Uh, yeah. True. That makes it even more simple. Just use
PARTCLAUSE_MATCH_NULLNESS.

David
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 6b635e8ad1..0c445a9bbe 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1810,11 +1810,62 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		PartClauseInfo *partclause;
 
+		/*
+		 * For bool tests in the form of partkey IS NOT true and IS NOT false,
+		 * we invert these clauses.  Effectively, "partkey IS NOT true" becomes
+		 * "partkey IS false OR partkey IS NULL".  We do this by building an OR
+		 * BoolExpr and forming a clause just like that and punt it off to
+		 * gen_partprune_steps_internal() to generate pruning steps.
+		 */
+		if (noteq)
+		{
+			List *new_clauses;
+			List *or_clause;
+			BooleanTest *new_booltest = copyObject(clause);
+			NullTest *nulltest;
+
+			/* We expect 'noteq' to only be set to true for BooleanTests */
+			Assert(IsA(clause, BooleanTest));
+
+			/* reverse the bool test */
+			if (new_booltest->booltesttype == IS_NOT_TRUE)
+				new_booltest->booltesttype = IS_FALSE;
+			else if (new_booltest->booltesttype == IS_NOT_FALSE)
+				new_booltest->booltesttype = IS_TRUE;
+			else
+			{
+				/*
+				 * We only expect match_boolean_partition_clause to match for
+				 * IS_NOT_TRUE and IS_NOT_FALSE.  IS_NOT_UNKNOWN is not
+				 * supported.
+				 */
+				Assert(false);
+			}
+
+			nulltest = makeNode(NullTest);
+			nulltest->arg = copyObject(partkey);
+			nulltest->nulltesttype = IS_NULL;
+			nulltest->argisrow = false;
+			nulltest->location = -1;
+
+			new_clauses = list_make2(new_booltest, nulltest);
+			or_clause = list_make1(makeBoolExpr(OR_EXPR, new_clauses, -1));
+
+			/* Finally, generate steps */
+			*clause_steps = gen_partprune_steps_internal(context, or_clause);
+
+			if (context->contradictory)
+				return PARTCLAUSE_MATCH_CONTRADICT; /* shouldn't happen */
+			else if (*clause_steps == NIL)
+				return PARTCLAUSE_UNSUPPORTED; /* step generation failed */
+			return PARTCLAUSE_MATCH_STEPS;
+		}
+
 		partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo));
 		partclause->keyno = partkeyidx;
 		/* Do pruning with the Boolean equality operator. */
 		partclause->opno = BooleanEqualOperator;
-		partclause->op_is_ne = noteq;
+		partclause->op_is_ne = false;
 		partclause->expr = expr;
 		/* We know that expr is of Boolean type. */
 		partclause->cmpfn = part_scheme->partsupfunc[partkeyidx].fn_oid;
@@ -2358,7 +2409,7 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
  * For LIST and RANGE partitioned tables, callers must ensure that
  * step_nullkeys is NULL, and that prefix contains at least one clause for
  * each of the partition keys prior to the key that 'step_lastexpr' and
- * 'step_lastcmpfn'belong to.
+ * 'step_lastcmpfn' belong to.
  *
  * For HASH partitioned tables, callers must ensure that 'prefix' contains at
  * least one clause for each of the partition keys apart from the final key
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 9a4c48c055..b41950d923 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1169,6 +1169,57 @@ select * from boolpart where a is not unknown;
  t
 (2 rows)
 
+-- try some other permutations with a NULL partition instead of a DEFAULT
+delete from boolpart where a is null;
+create table boolpart_null partition of boolpart for values in (null);
+insert into boolpart values(null);
+explain (costs off) select * from boolpart where a is not true;
+                 QUERY PLAN                 
+--------------------------------------------
+ Append
+   ->  Seq Scan on boolpart_f boolpart_1
+         Filter: (a IS NOT TRUE)
+   ->  Seq Scan on boolpart_null boolpart_2
+         Filter: (a IS NOT TRUE)
+(5 rows)
+
+explain (costs off) select * from boolpart where a is not true and a is not false;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Seq Scan on boolpart_null boolpart
+   Filter: ((a IS NOT TRUE) AND (a IS NOT FALSE))
+(2 rows)
+
+explain (costs off) select * from boolpart where a is not false;
+                 QUERY PLAN                 
+--------------------------------------------
+ Append
+   ->  Seq Scan on boolpart_t boolpart_1
+         Filter: (a IS NOT FALSE)
+   ->  Seq Scan on boolpart_null boolpart_2
+         Filter: (a IS NOT FALSE)
+(5 rows)
+
+select * from boolpart where a is not true;
+ a 
+---
+ f
+ 
+(2 rows)
+
+select * from boolpart where a is not true and a is not false;
+ a 
+---
+ 
+(1 row)
+
+select * from boolpart where a is not false;
+ a 
+---
+ t
+ 
+(2 rows)
+
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
 create table iboolpart (a bool) partition by list ((not a));
@@ -1315,11 +1366,37 @@ select * from iboolpart where a is not unknown;
  f
 (2 rows)
 
+-- Try some other permutations with a NULL partition instead of a DEFAULT
+delete from iboolpart where a is null;
+create table iboolpart_null partition of iboolpart for values in (null);
+insert into iboolpart values(null);
+-- Pruning shouldn't take place for these.  Just check the result is correct
+select * from iboolpart where a is not true;
+ a 
+---
+ f
+ 
+(2 rows)
+
+select * from iboolpart where a is not true and a is not false;
+ a 
+---
+ 
+(1 row)
+
+select * from iboolpart where a is not false;
+ a 
+---
+ t
+ 
+(2 rows)
+
 create table boolrangep (a bool, b bool, c int) partition by range (a,b,c);
 create table boolrangep_tf partition of boolrangep for values from ('true', 'false', 0) to ('true', 'false', 100);
 create table boolrangep_ft partition of boolrangep for values from ('false', 'true', 0) to ('false', 'true', 100);
 create table boolrangep_ff1 partition of boolrangep for values from ('false', 'false', 0) to ('false', 'false', 50);
 create table boolrangep_ff2 partition of boolrangep for values from ('false', 'false', 50) to ('false', 'false', 100);
+create table boolrangep_null partition of boolrangep default;
 -- try a more complex case that's been known to trip up pruning in the past
 explain (costs off)  select * from boolrangep where not a and not b and c = 25;
                   QUERY PLAN                  
@@ -1328,6 +1405,32 @@ explain (costs off)  select * from boolrangep where not a and not b and c = 25;
    Filter: ((NOT a) AND (NOT b) AND (c = 25))
 (2 rows)
 
+-- ensure we prune boolrangep_tf
+explain (costs off)  select * from boolrangep where a is not true and not b and c = 25;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Append
+   ->  Seq Scan on boolrangep_ff1 boolrangep_1
+         Filter: ((a IS NOT TRUE) AND (NOT b) AND (c = 25))
+   ->  Seq Scan on boolrangep_ff2 boolrangep_2
+         Filter: ((a IS NOT TRUE) AND (NOT b) AND (c = 25))
+   ->  Seq Scan on boolrangep_ft boolrangep_3
+         Filter: ((a IS NOT TRUE) AND (NOT b) AND (c = 25))
+   ->  Seq Scan on boolrangep_null boolrangep_4
+         Filter: ((a IS NOT TRUE) AND (NOT b) AND (c = 25))
+(9 rows)
+
+-- ensure we prune everything apart from boolrangep_tf and boolrangep_null
+explain (costs off)  select * from boolrangep where a is not false and not b and c = 25;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Append
+   ->  Seq Scan on boolrangep_tf boolrangep_1
+         Filter: ((a IS NOT FALSE) AND (NOT b) AND (c = 25))
+   ->  Seq Scan on boolrangep_null boolrangep_2
+         Filter: ((a IS NOT FALSE) AND (NOT b) AND (c = 25))
+(5 rows)
+
 -- test scalar-to-array operators
 create table coercepart (a varchar) partition by list (a);
 create table coercepart_ab partition of coercepart for values in ('ab');
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 7bf3920827..7ba6a9ff37 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -178,6 +178,19 @@ select * from boolpart where a is not true and a is not false;
 select * from boolpart where a is unknown;
 select * from boolpart where a is not unknown;
 
+-- try some other permutations with a NULL partition instead of a DEFAULT
+delete from boolpart where a is null;
+create table boolpart_null partition of boolpart for values in (null);
+insert into boolpart values(null);
+
+explain (costs off) select * from boolpart where a is not true;
+explain (costs off) select * from boolpart where a is not true and a is not false;
+explain (costs off) select * from boolpart where a is not false;
+
+select * from boolpart where a is not true;
+select * from boolpart where a is not true and a is not false;
+select * from boolpart where a is not false;
+
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
 create table iboolpart (a bool) partition by list ((not a));
@@ -204,15 +217,32 @@ select * from iboolpart where a is not true and a is not false;
 select * from iboolpart where a is unknown;
 select * from iboolpart where a is not unknown;
 
+-- Try some other permutations with a NULL partition instead of a DEFAULT
+delete from iboolpart where a is null;
+create table iboolpart_null partition of iboolpart for values in (null);
+insert into iboolpart values(null);
+
+-- Pruning shouldn't take place for these.  Just check the result is correct
+select * from iboolpart where a is not true;
+select * from iboolpart where a is not true and a is not false;
+select * from iboolpart where a is not false;
+
 create table boolrangep (a bool, b bool, c int) partition by range (a,b,c);
 create table boolrangep_tf partition of boolrangep for values from ('true', 'false', 0) to ('true', 'false', 100);
 create table boolrangep_ft partition of boolrangep for values from ('false', 'true', 0) to ('false', 'true', 100);
 create table boolrangep_ff1 partition of boolrangep for values from ('false', 'false', 0) to ('false', 'false', 50);
 create table boolrangep_ff2 partition of boolrangep for values from ('false', 'false', 50) to ('false', 'false', 100);
+create table boolrangep_null partition of boolrangep default;
 
 -- try a more complex case that's been known to trip up pruning in the past
 explain (costs off)  select * from boolrangep where not a and not b and c = 25;
 
+-- ensure we prune boolrangep_tf
+explain (costs off)  select * from boolrangep where a is not true and not b and c = 25;
+
+-- ensure we prune everything apart from boolrangep_tf and boolrangep_null
+explain (costs off)  select * from boolrangep where a is not false and not b and c = 25;
+
 -- test scalar-to-array operators
 create table coercepart (a varchar) partition by list (a);
 create table coercepart_ab partition of coercepart for values in ('ab');


Attachments:

  [text/plain] fix_partprune_BooleanTests.patch (10.7K, ../../CAApHDvpW6N38TerNpHuvXSZ3NHMpQr+G0R8fqvj1wD9ocCeRsA@mail.gmail.com/2-fix_partprune_BooleanTests.patch)
  download | inline diff:
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 6b635e8ad1..0c445a9bbe 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1810,11 +1810,62 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 	{
 		PartClauseInfo *partclause;
 
+		/*
+		 * For bool tests in the form of partkey IS NOT true and IS NOT false,
+		 * we invert these clauses.  Effectively, "partkey IS NOT true" becomes
+		 * "partkey IS false OR partkey IS NULL".  We do this by building an OR
+		 * BoolExpr and forming a clause just like that and punt it off to
+		 * gen_partprune_steps_internal() to generate pruning steps.
+		 */
+		if (noteq)
+		{
+			List *new_clauses;
+			List *or_clause;
+			BooleanTest *new_booltest = copyObject(clause);
+			NullTest *nulltest;
+
+			/* We expect 'noteq' to only be set to true for BooleanTests */
+			Assert(IsA(clause, BooleanTest));
+
+			/* reverse the bool test */
+			if (new_booltest->booltesttype == IS_NOT_TRUE)
+				new_booltest->booltesttype = IS_FALSE;
+			else if (new_booltest->booltesttype == IS_NOT_FALSE)
+				new_booltest->booltesttype = IS_TRUE;
+			else
+			{
+				/*
+				 * We only expect match_boolean_partition_clause to match for
+				 * IS_NOT_TRUE and IS_NOT_FALSE.  IS_NOT_UNKNOWN is not
+				 * supported.
+				 */
+				Assert(false);
+			}
+
+			nulltest = makeNode(NullTest);
+			nulltest->arg = copyObject(partkey);
+			nulltest->nulltesttype = IS_NULL;
+			nulltest->argisrow = false;
+			nulltest->location = -1;
+
+			new_clauses = list_make2(new_booltest, nulltest);
+			or_clause = list_make1(makeBoolExpr(OR_EXPR, new_clauses, -1));
+
+			/* Finally, generate steps */
+			*clause_steps = gen_partprune_steps_internal(context, or_clause);
+
+			if (context->contradictory)
+				return PARTCLAUSE_MATCH_CONTRADICT; /* shouldn't happen */
+			else if (*clause_steps == NIL)
+				return PARTCLAUSE_UNSUPPORTED; /* step generation failed */
+			return PARTCLAUSE_MATCH_STEPS;
+		}
+
 		partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo));
 		partclause->keyno = partkeyidx;
 		/* Do pruning with the Boolean equality operator. */
 		partclause->opno = BooleanEqualOperator;
-		partclause->op_is_ne = noteq;
+		partclause->op_is_ne = false;
 		partclause->expr = expr;
 		/* We know that expr is of Boolean type. */
 		partclause->cmpfn = part_scheme->partsupfunc[partkeyidx].fn_oid;
@@ -2358,7 +2409,7 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
  * For LIST and RANGE partitioned tables, callers must ensure that
  * step_nullkeys is NULL, and that prefix contains at least one clause for
  * each of the partition keys prior to the key that 'step_lastexpr' and
- * 'step_lastcmpfn'belong to.
+ * 'step_lastcmpfn' belong to.
  *
  * For HASH partitioned tables, callers must ensure that 'prefix' contains at
  * least one clause for each of the partition keys apart from the final key
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 9a4c48c055..b41950d923 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -1169,6 +1169,57 @@ select * from boolpart where a is not unknown;
  t
 (2 rows)
 
+-- try some other permutations with a NULL partition instead of a DEFAULT
+delete from boolpart where a is null;
+create table boolpart_null partition of boolpart for values in (null);
+insert into boolpart values(null);
+explain (costs off) select * from boolpart where a is not true;
+                 QUERY PLAN                 
+--------------------------------------------
+ Append
+   ->  Seq Scan on boolpart_f boolpart_1
+         Filter: (a IS NOT TRUE)
+   ->  Seq Scan on boolpart_null boolpart_2
+         Filter: (a IS NOT TRUE)
+(5 rows)
+
+explain (costs off) select * from boolpart where a is not true and a is not false;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Seq Scan on boolpart_null boolpart
+   Filter: ((a IS NOT TRUE) AND (a IS NOT FALSE))
+(2 rows)
+
+explain (costs off) select * from boolpart where a is not false;
+                 QUERY PLAN                 
+--------------------------------------------
+ Append
+   ->  Seq Scan on boolpart_t boolpart_1
+         Filter: (a IS NOT FALSE)
+   ->  Seq Scan on boolpart_null boolpart_2
+         Filter: (a IS NOT FALSE)
+(5 rows)
+
+select * from boolpart where a is not true;
+ a 
+---
+ f
+ 
+(2 rows)
+
+select * from boolpart where a is not true and a is not false;
+ a 
+---
+ 
+(1 row)
+
+select * from boolpart where a is not false;
+ a 
+---
+ t
+ 
+(2 rows)
+
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
 create table iboolpart (a bool) partition by list ((not a));
@@ -1315,11 +1366,37 @@ select * from iboolpart where a is not unknown;
  f
 (2 rows)
 
+-- Try some other permutations with a NULL partition instead of a DEFAULT
+delete from iboolpart where a is null;
+create table iboolpart_null partition of iboolpart for values in (null);
+insert into iboolpart values(null);
+-- Pruning shouldn't take place for these.  Just check the result is correct
+select * from iboolpart where a is not true;
+ a 
+---
+ f
+ 
+(2 rows)
+
+select * from iboolpart where a is not true and a is not false;
+ a 
+---
+ 
+(1 row)
+
+select * from iboolpart where a is not false;
+ a 
+---
+ t
+ 
+(2 rows)
+
 create table boolrangep (a bool, b bool, c int) partition by range (a,b,c);
 create table boolrangep_tf partition of boolrangep for values from ('true', 'false', 0) to ('true', 'false', 100);
 create table boolrangep_ft partition of boolrangep for values from ('false', 'true', 0) to ('false', 'true', 100);
 create table boolrangep_ff1 partition of boolrangep for values from ('false', 'false', 0) to ('false', 'false', 50);
 create table boolrangep_ff2 partition of boolrangep for values from ('false', 'false', 50) to ('false', 'false', 100);
+create table boolrangep_null partition of boolrangep default;
 -- try a more complex case that's been known to trip up pruning in the past
 explain (costs off)  select * from boolrangep where not a and not b and c = 25;
                   QUERY PLAN                  
@@ -1328,6 +1405,32 @@ explain (costs off)  select * from boolrangep where not a and not b and c = 25;
    Filter: ((NOT a) AND (NOT b) AND (c = 25))
 (2 rows)
 
+-- ensure we prune boolrangep_tf
+explain (costs off)  select * from boolrangep where a is not true and not b and c = 25;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Append
+   ->  Seq Scan on boolrangep_ff1 boolrangep_1
+         Filter: ((a IS NOT TRUE) AND (NOT b) AND (c = 25))
+   ->  Seq Scan on boolrangep_ff2 boolrangep_2
+         Filter: ((a IS NOT TRUE) AND (NOT b) AND (c = 25))
+   ->  Seq Scan on boolrangep_ft boolrangep_3
+         Filter: ((a IS NOT TRUE) AND (NOT b) AND (c = 25))
+   ->  Seq Scan on boolrangep_null boolrangep_4
+         Filter: ((a IS NOT TRUE) AND (NOT b) AND (c = 25))
+(9 rows)
+
+-- ensure we prune everything apart from boolrangep_tf and boolrangep_null
+explain (costs off)  select * from boolrangep where a is not false and not b and c = 25;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Append
+   ->  Seq Scan on boolrangep_tf boolrangep_1
+         Filter: ((a IS NOT FALSE) AND (NOT b) AND (c = 25))
+   ->  Seq Scan on boolrangep_null boolrangep_2
+         Filter: ((a IS NOT FALSE) AND (NOT b) AND (c = 25))
+(5 rows)
+
 -- test scalar-to-array operators
 create table coercepart (a varchar) partition by list (a);
 create table coercepart_ab partition of coercepart for values in ('ab');
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 7bf3920827..7ba6a9ff37 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -178,6 +178,19 @@ select * from boolpart where a is not true and a is not false;
 select * from boolpart where a is unknown;
 select * from boolpart where a is not unknown;
 
+-- try some other permutations with a NULL partition instead of a DEFAULT
+delete from boolpart where a is null;
+create table boolpart_null partition of boolpart for values in (null);
+insert into boolpart values(null);
+
+explain (costs off) select * from boolpart where a is not true;
+explain (costs off) select * from boolpart where a is not true and a is not false;
+explain (costs off) select * from boolpart where a is not false;
+
+select * from boolpart where a is not true;
+select * from boolpart where a is not true and a is not false;
+select * from boolpart where a is not false;
+
 -- inverse boolean partitioning - a seemingly unlikely design, but we've got
 -- code for it, so we'd better test it.
 create table iboolpart (a bool) partition by list ((not a));
@@ -204,15 +217,32 @@ select * from iboolpart where a is not true and a is not false;
 select * from iboolpart where a is unknown;
 select * from iboolpart where a is not unknown;
 
+-- Try some other permutations with a NULL partition instead of a DEFAULT
+delete from iboolpart where a is null;
+create table iboolpart_null partition of iboolpart for values in (null);
+insert into iboolpart values(null);
+
+-- Pruning shouldn't take place for these.  Just check the result is correct
+select * from iboolpart where a is not true;
+select * from iboolpart where a is not true and a is not false;
+select * from iboolpart where a is not false;
+
 create table boolrangep (a bool, b bool, c int) partition by range (a,b,c);
 create table boolrangep_tf partition of boolrangep for values from ('true', 'false', 0) to ('true', 'false', 100);
 create table boolrangep_ft partition of boolrangep for values from ('false', 'true', 0) to ('false', 'true', 100);
 create table boolrangep_ff1 partition of boolrangep for values from ('false', 'false', 0) to ('false', 'false', 50);
 create table boolrangep_ff2 partition of boolrangep for values from ('false', 'false', 50) to ('false', 'false', 100);
+create table boolrangep_null partition of boolrangep default;
 
 -- try a more complex case that's been known to trip up pruning in the past
 explain (costs off)  select * from boolrangep where not a and not b and c = 25;
 
+-- ensure we prune boolrangep_tf
+explain (costs off)  select * from boolrangep where a is not true and not b and c = 25;
+
+-- ensure we prune everything apart from boolrangep_tf and boolrangep_null
+explain (costs off)  select * from boolrangep where a is not false and not b and c = 25;
+
 -- test scalar-to-array operators
 create table coercepart (a varchar) partition by list (a);
 create table coercepart_ab partition of coercepart for values in ('ab');


^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 12:53     ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 16:24       ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-18 23:49         ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
@ 2024-02-19 08:21           ` Tender Wang <tndrwang@gmail.com>
  2 siblings, 0 replies; 12+ messages in thread

From: Tender Wang @ 2024-02-19 08:21 UTC (permalink / raw)
  To: David Rowley <dgrowleyml@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; exclusion@gmail.com; pgsql-bugs@lists.postgresql.org

David Rowley <dgrowleyml@gmail.com> 于2024年2月19日周一 07:49写道:

> On Mon, 19 Feb 2024 at 05:25, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> >
> > David Rowley <dgrowleyml@gmail.com> writes:
> > > We can do the same for BooleanTests. Given a clause such as: "partkey
> > > IS NOT false", we can just generate the clause "partkey IS true OR
> > > partkey IS NULL" and recursively generate steps for that.
> >
> > +1 ... sounds clean and clearly correct.
>
> Here's a more complete patch for this.  I included some tests for LIST
> and RANGE partitioned tables. I did manual testing for HASH, and was
> on the fence about covering that too.
>
> I did try the following using the table from the tests:
>
> select * from boolrangep where a is not true and not b and c = 25 and
> a is not null;
>
> When will be effectively transformed into:
>
> select * from boolrangep where (a is false or a is null) and not b and
> c = 25 and a is not null;
>
> It seems that's unable to prune the NULL partition but that mostly
> seems to be due to a limitation of the current design. I'm not sure
> it's worth going to any additional trouble to make that work.  It
> seems a bit unlikely, especially so given how long the BooleanTest
> pruning stuff was broken for before anyone noticed.
>
> > > I'm tempted to go a bit further in master only and add support for
> > > bool IS NOT UNKNOWN and bool IS UNKNOWN using the same method.
> >
> > These are the same as IS NOT NULL and IS NULL, so I don't see the
> > need for an OR?
>
> Uh, yeah. True. That makes it even more simple. Just use
> PARTCLAUSE_MATCH_NULLNESS.
>
> David
>

After git apply fix_partprune_BooleanTests.patch on master, I got below
warnings:

partprune.c: In function ‘match_clause_to_partition_key’:
../../../src/include/nodes/nodes.h:221:25: warning: initialization of
‘BooleanTest *’ {aka ‘struct BooleanTest *’} from incompatible pointer type
‘Expr *’ {aka ‘struct Expr *’} [-Wincompatible-pointer-types]
  221 | #define copyObject(obj) ((typeof(obj)) copyObjectImpl(obj))
      |                         ^
partprune.c:1824:32: note: in expansion of macro ‘copyObject’
 1824 |    BooleanTest *new_booltest = copyObject(clause);

Maybe this: BooleanTest *new_booltest = (BooleanTest *) copyObject(clause);


-- 
Tender Wang
OpenPie:  https://en.openpie.com/

^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 12:53     ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 16:24       ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-18 23:49         ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
@ 2024-02-19 09:45           ` Amit Langote <amitlangote09@gmail.com>
  2 siblings, 0 replies; 12+ messages in thread

From: Amit Langote @ 2024-02-19 09:45 UTC (permalink / raw)
  To: David Rowley <dgrowleyml@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; exclusion@gmail.com; pgsql-bugs@lists.postgresql.org

Hi David,

On Mon, Feb 19, 2024 at 8:49 AM David Rowley <dgrowleyml@gmail.com> wrote:
> On Mon, 19 Feb 2024 at 05:25, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> >
> > David Rowley <dgrowleyml@gmail.com> writes:
> > > We can do the same for BooleanTests. Given a clause such as: "partkey
> > > IS NOT false", we can just generate the clause "partkey IS true OR
> > > partkey IS NULL" and recursively generate steps for that.
> >
> > +1 ... sounds clean and clearly correct.
>
> Here's a more complete patch for this.

Thanks for working on this.

Overall, I too like this idea.

Though I noticed that this approach will effectively disable pruning
with a clause on the 2nd key column, if any, present in the query:

CREATE TABLE t (b bool, i int) PARTITION BY RANGE (b, i);
CREATE TABLE tp PARTITION OF t FOR VALUES FROM (false, 0) TO (false, 1);
CREATE TABLE tp2 PARTITION OF t FOR VALUES FROM (false, 1) TO (false, 2);
CREATE TABLE tp3 PARTITION OF t FOR VALUES FROM (true, 0) TO (true, 1);
CREATE TABLE tp4 PARTITION OF t FOR VALUES FROM (true, 1) TO (true, 2);

-- tp2 should be pruned, but is not.
explain SELECT * FROM t WHERE b IS NOT true and i = 0;
                          QUERY PLAN
--------------------------------------------------------------
 Append  (cost=0.00..81.81 rows=12 width=5)
   ->  Seq Scan on tp t_1  (cost=0.00..40.88 rows=6 width=5)
         Filter: ((b IS NOT TRUE) AND (i = 0))
   ->  Seq Scan on tp2 t_2  (cost=0.00..40.88 rows=6 width=5)
         Filter: ((b IS NOT TRUE) AND (i = 0))
(5 rows)

-- like it is in this case
explain SELECT * FROM t WHERE b IS false and i = 0;
                     QUERY PLAN
-----------------------------------------------------
 Seq Scan on tp t  (cost=0.00..40.88 rows=6 width=5)
   Filter: ((b IS FALSE) AND (i = 0))
(2 rows)

I guess we'll have to live with that, because the generate_opsteps
code that generates multi-column pruning steps only supports scenarios
where each key's matched clause is a simple comparison, not, for
example, where it is an OR expression.

-- 
Thanks, Amit Langote





^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 12:53     ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 16:24       ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-18 23:49         ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
@ 2024-02-20 03:00           ` Alexander Lakhin <exclusion@gmail.com>
  2024-02-20 03:50             ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2 siblings, 1 reply; 12+ messages in thread

From: Alexander Lakhin @ 2024-02-20 03:00 UTC (permalink / raw)
  To: David Rowley <dgrowleyml@gmail.com>; Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-bugs@lists.postgresql.org

Hello David,

19.02.2024 02:49, David Rowley wrote:
>
> Here's a more complete patch for this.  I included some tests for LIST
> and RANGE partitioned tables. I did manual testing for HASH, and was
> on the fence about covering that too.
>

Thank you for the fix!

Beside that, I'm a bit confused by the opstrategy description for
get_matching_range_bounds().
Above that function we have:
  * 'opstrategy' if non-zero must be a btree strategy number.

But as we could see, zero opstrategy is not valid for the function (so
"if non-zero" is meaningless here?), unlike opstrategy for
get_matching_list_bounds(), which has the same description, but the latter
function contains:
     /* Special case handling of values coming from a <> operator clause. */
     if (opstrategy == InvalidStrategy)
...

Best regards,
Alexander





^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 12:53     ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 16:24       ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-18 23:49         ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-20 03:00           ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Alexander Lakhin <exclusion@gmail.com>
@ 2024-02-20 03:50             ` David Rowley <dgrowleyml@gmail.com>
  2024-02-20 05:39               ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  0 siblings, 1 reply; 12+ messages in thread

From: David Rowley @ 2024-02-20 03:50 UTC (permalink / raw)
  To: Alexander Lakhin <exclusion@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-bugs@lists.postgresql.org

On Tue, 20 Feb 2024 at 16:00, Alexander Lakhin <exclusion@gmail.com> wrote:
> Beside that, I'm a bit confused by the opstrategy description for
> get_matching_range_bounds().
> Above that function we have:
>   * 'opstrategy' if non-zero must be a btree strategy number.
>
> But as we could see, zero opstrategy is not valid for the function (so
> "if non-zero" is meaningless here?), unlike opstrategy for
> get_matching_list_bounds(), which has the same description, but the latter
> function contains:
>      /* Special case handling of values coming from a <> operator clause. */
>      if (opstrategy == InvalidStrategy)

Yeah, that seems worth fixing in master as, I agree, the comment is
wrong.  Possibly, we considered supporting <> for RANGE partitioning
at some point, I don't recall.

I was also working on a fix for what I mentioned in [1], which, I
think, is master-only material.  I'd say we can fix the comment as
part of that.

The patch for both is attached.

David

[1] https://postgr.es/m/CAApHDvqriy8mPOFJ_Bd66YGXJ4+XULpv-4YdB+ePdCQFztyisA@mail.gmail.com
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index ccb9a579b3..a19f596d4d 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1936,9 +1936,11 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 		 * whatsoever, but their negators (equality) are.  We can use one of
 		 * those if we find it, but only for list partitioning.
 		 *
-		 * Note: we report NOMATCH on failure, in case a later partkey has the
-		 * same expression but different opfamily.  That's unlikely, but not
-		 * much more so than duplicate expressions with different collations.
+		 * Note: we report NOMATCH on failure if the negator isn't the
+		 * equality operator for the partkey's opfamily as other partkeys may
+		 * have the same expression but different opfamily.  That's unlikely,
+		 * but not much more so than duplicate expressions with different
+		 * collations.
 		 */
 		if (op_in_opfamily(opno, partopfamily))
 		{
@@ -1948,8 +1950,9 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 		}
 		else
 		{
+			/* not supported for anything apart from LIST partitioned tables */
 			if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
-				return PARTCLAUSE_NOMATCH;
+				return PARTCLAUSE_UNSUPPORTED;
 
 			/* See if the negator is equality */
 			negator = get_negator(opno);
@@ -2924,7 +2927,7 @@ get_matching_list_bounds(PartitionPruneContext *context,
  * multiple pruning steps might exclude it, so we infer its inclusion
  * elsewhere.
  *
- * 'opstrategy' if non-zero must be a btree strategy number.
+ * 'opstrategy' must be a btree strategy number.
  *
  * 'values' contains Datums indexed by the partition key to use for pruning.
  *


Attachments:

  [text/plain] minor_partpruning_fixes.patch (1.7K, ../../CAApHDvqcY53LqR4bH2Cz3XHjCc7ZbbX0UptBCMhuwqbN-e8SRA@mail.gmail.com/2-minor_partpruning_fixes.patch)
  download | inline diff:
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index ccb9a579b3..a19f596d4d 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1936,9 +1936,11 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 		 * whatsoever, but their negators (equality) are.  We can use one of
 		 * those if we find it, but only for list partitioning.
 		 *
-		 * Note: we report NOMATCH on failure, in case a later partkey has the
-		 * same expression but different opfamily.  That's unlikely, but not
-		 * much more so than duplicate expressions with different collations.
+		 * Note: we report NOMATCH on failure if the negator isn't the
+		 * equality operator for the partkey's opfamily as other partkeys may
+		 * have the same expression but different opfamily.  That's unlikely,
+		 * but not much more so than duplicate expressions with different
+		 * collations.
 		 */
 		if (op_in_opfamily(opno, partopfamily))
 		{
@@ -1948,8 +1950,9 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 		}
 		else
 		{
+			/* not supported for anything apart from LIST partitioned tables */
 			if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
-				return PARTCLAUSE_NOMATCH;
+				return PARTCLAUSE_UNSUPPORTED;
 
 			/* See if the negator is equality */
 			negator = get_negator(opno);
@@ -2924,7 +2927,7 @@ get_matching_list_bounds(PartitionPruneContext *context,
  * multiple pruning steps might exclude it, so we infer its inclusion
  * elsewhere.
  *
- * 'opstrategy' if non-zero must be a btree strategy number.
+ * 'opstrategy' must be a btree strategy number.
  *
  * 'values' contains Datums indexed by the partition key to use for pruning.
  *


^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy
  2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
  2024-02-15 16:28 ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-16 12:32   ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 12:53     ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-18 16:24       ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Tom Lane <tgl@sss.pgh.pa.us>
  2024-02-18 23:49         ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
  2024-02-20 03:00           ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy Alexander Lakhin <exclusion@gmail.com>
  2024-02-20 03:50             ` Re: BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy David Rowley <dgrowleyml@gmail.com>
@ 2024-02-20 05:39               ` David Rowley <dgrowleyml@gmail.com>
  0 siblings, 0 replies; 12+ messages in thread

From: David Rowley @ 2024-02-20 05:39 UTC (permalink / raw)
  To: Alexander Lakhin <exclusion@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Amit Langote <amitlan@postgresql.org>; Alvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-bugs@lists.postgresql.org

On Tue, 20 Feb 2024 at 16:50, David Rowley <dgrowleyml@gmail.com> wrote:
>
> On Tue, 20 Feb 2024 at 16:00, Alexander Lakhin <exclusion@gmail.com> wrote:
> > Beside that, I'm a bit confused by the opstrategy description for
> > get_matching_range_bounds().
> > Above that function we have:
> >   * 'opstrategy' if non-zero must be a btree strategy number.
> >

> Yeah, that seems worth fixing in master as, I agree, the comment is
> wrong.  Possibly, we considered supporting <> for RANGE partitioning
> at some point, I don't recall.
>
> I was also working on a fix for what I mentioned in [1], which, I
> think, is master-only material.  I'd say we can fix the comment as
> part of that.
>
> The patch for both is attached.

I've pushed this patch.

David





^ permalink  raw  reply  [nested|flat] 12+ messages in thread


end of thread, other threads:[~2024-02-20 05:39 UTC | newest]

Thread overview: 12+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-02-15 09:00 BUG #18344: Pruning tables partitioned by bool range fails with invalid strategy PG Bug reporting form <noreply@postgresql.org>
2024-02-15 16:28 ` Tom Lane <tgl@sss.pgh.pa.us>
2024-02-16 02:56   ` David Rowley <dgrowleyml@gmail.com>
2024-02-16 12:32   ` David Rowley <dgrowleyml@gmail.com>
2024-02-18 12:53     ` David Rowley <dgrowleyml@gmail.com>
2024-02-18 16:24       ` Tom Lane <tgl@sss.pgh.pa.us>
2024-02-18 23:49         ` David Rowley <dgrowleyml@gmail.com>
2024-02-19 08:21           ` Tender Wang <tndrwang@gmail.com>
2024-02-19 09:45           ` Amit Langote <amitlangote09@gmail.com>
2024-02-20 03:00           ` Alexander Lakhin <exclusion@gmail.com>
2024-02-20 03:50             ` David Rowley <dgrowleyml@gmail.com>
2024-02-20 05:39               ` David Rowley <dgrowleyml@gmail.com>

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox