From d21d4f1ca0c44ecf9f75c0519e1c034d0fb3b630 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Fri, 18 Sep 2026 17:15:52 +1200 Subject: [PATCH] Fix pruning of DEFAULT partition in RANGE partitioned tables Some code added in 489247b0e tried to prune the DEFAULT partition when the next partition had a MINVALUE clause and likewise when the final partition to scan had a MAXVALUE clause for the final partition key in the pruning step. This code was incorrect as it could prune the default partition incorrectly in cases such as: p: PARTITION BY RANGE (a, b) p1: FOR VALUES FROM (13, 0) TO (19, MAXVALUE) pd: DEFAULT SELECT * FROM t WHERE a = 32 AND b >= -7; Here the pruning step for a = 32 and b >= -7 would see that only the default partition needs to be scan, but it would then see that the partition prior to the default had a MAXVALUE bound then prune away the default thinking that it needn't be scanned. This could result in incorrect results. Fix this by moving the code that looks for the MAXVALUE bound into the code handling BTEqualStrategyNumber so that when we're pruning with a prefix of the partition keys, we check if the bound for the offset we've calculated lands on a partition where the next partition key is bounded with MAXVALUE. If so we don't include the default partition. This leaves only the case of the first partition key. We handle that by modifying the existing code that was checking the last key covered by the given values. Author: Ewan Young Reviewed-by: Tender Wang Reviewed-by: David Rowley Discussion: https://postgr.es/m/CAON2xHO=sqdqp=z8zWkybnWp0AuvefnAi2ez2vOYWrhXB6hHWQ@mail.gmail.com Backpatch-through: 14 --- src/backend/partitioning/partprune.c | 66 +++++++------ src/test/regress/expected/partition_prune.out | 94 +++++++++++++++++++ src/test/regress/sql/partition_prune.sql | 47 ++++++++++ 3 files changed, 179 insertions(+), 28 deletions(-) diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c index bb4081829ad..e10908bf9db 100644 --- a/src/backend/partitioning/partprune.c +++ b/src/backend/partitioning/partprune.c @@ -3104,9 +3104,17 @@ get_matching_range_bounds(PartitionPruneContext *context, /* * off + 1, then would be the offset of the greatest bound - * to be included in the result. + * to be included in the result. The exception is the + * mirror image of the MINVALUE case above: if the matched + * bound is exactly (prefix, MAXVALUE), no row carrying + * this prefix can sort above it, so the key space beyond + * it need not be considered. */ - maxoff = off + 1; + if (boundinfo->kind[off][nvalues] == + PARTITION_RANGE_DATUM_MAXVALUE) + maxoff = off; + else + maxoff = off + 1; } Assert(minoff >= 0 && maxoff >= 0); @@ -3297,43 +3305,45 @@ get_matching_range_bounds(PartitionPruneContext *context, Assert(minoff >= 0 && minoff <= boundinfo->ndatums); Assert(maxoff >= 0 && maxoff <= boundinfo->ndatums); - /* - * If the smallest partition to return has MINVALUE (negative infinity) as - * its lower bound, increment it to point to the next finite bound - * (supposedly its upper bound), so that we don't inadvertently end up - * scanning the default partition. - */ - if (minoff < boundinfo->ndatums && partindices[minoff] < 0) - { - int lastkey = nvalues - 1; - if (boundinfo->kind[minoff][lastkey] == - PARTITION_RANGE_DATUM_MINVALUE) + /* + * Check for cases that we're scanning the DEFAULT partition when no rows + * can exist there for the given value. This can happen when the first or + * final bound for partitions we're scanning have a MINVALUE or MAXVALUE + * clause respectively. We needn't scan the DEFAULT partition for values + * beyond the bound since the MINVALUE / MAXVALUE bound handles up to + * negative or positive infinity. The BTEqualStrategyNumber code above + * handled doing this for subsequent partition keys, so all that's left to + * handle here is the same for the first partition key. We needn't + * perform this for all steps as the intersected results from the + * recursive processing of pruning steps for leading keys means we only + * scan the partitions common to all steps in the intersected set of + * steps. + */ + if (nvalues == 1) + { + /* + * Check if we're scanning the default and if a MINVALUE bound covers + * the key space for the lower end. + */ + if (minoff < boundinfo->ndatums && partindices[minoff] < 0 && + boundinfo->kind[minoff][0] == PARTITION_RANGE_DATUM_MINVALUE) { minoff++; Assert(boundinfo->indexes[minoff] >= 0); } - } - /* - * If the previous greatest partition has MAXVALUE (positive infinity) as - * its upper bound (something only possible to do with multi-column range - * partitioning), we scan switch to it as the greatest partition to - * return. Again, so that we don't inadvertently end up scanning the - * default partition. - */ - if (maxoff >= 1 && partindices[maxoff] < 0) - { - int lastkey = nvalues - 1; - - if (boundinfo->kind[maxoff - 1][lastkey] == - PARTITION_RANGE_DATUM_MAXVALUE) + /* + * Likewise, do the same check for the upper range partition if the + * prior bound is a MAXVALUE kind. + */ + if (maxoff >= 1 && partindices[maxoff] < 0 && + boundinfo->kind[maxoff - 1][0] == PARTITION_RANGE_DATUM_MAXVALUE) { maxoff--; Assert(boundinfo->indexes[maxoff] >= 0); } } - Assert(minoff >= 0 && maxoff >= 0); if (minoff <= maxoff) result->bound_offsets = bms_add_range(NULL, minoff, maxoff); diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index 86bb75b5917..58af1f8454e 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -1112,6 +1112,100 @@ explain (costs off) select count(*) from mc2bp where c1 > 7; (3 rows) drop table mc2bp; +-- Ensure the default partition is not pruned when a bound is unbounded only +-- in a trailing key +create table mc2pdmax (a int, b int) partition by range (a, b); +create table mc2pdmax_default partition of mc2pdmax default; +create table mc2pdmax1 partition of mc2pdmax for values from (13, 0) to (19, maxvalue); +-- Ensure we only scan the default partition in the following two cases +explain (costs off) select * from mc2pdmax where a = 32 and b >= -7; + QUERY PLAN +----------------------------------------------- + Seq Scan on mc2pdmax_default mc2pdmax + Filter: ((b >= '-7'::integer) AND (a = 32)) +(2 rows) + +explain (costs off) select * from mc2pdmax where a >= 25 and b >= 0; + QUERY PLAN +--------------------------------------- + Seq Scan on mc2pdmax_default mc2pdmax + Filter: ((a >= 25) AND (b >= 0)) +(2 rows) + +-- Ensure we don't scan the default when the MAXVALUE bound makes it so +-- no rows matching the WHERE clause can exist there. +explain (costs off) select * from mc2pdmax where a = 19 and b >= 5; + QUERY PLAN +----------------------------------- + Seq Scan on mc2pdmax1 mc2pdmax + Filter: ((b >= 5) AND (a = 19)) +(2 rows) + +-- Ensure we do scan the default to get the values with a > 19. +explain (costs off) select * from mc2pdmax where a >= 19 and b >= 5; + QUERY PLAN +----------------------------------------------- + Append + -> Seq Scan on mc2pdmax1 mc2pdmax_1 + Filter: ((a >= 19) AND (b >= 5)) + -> Seq Scan on mc2pdmax_default mc2pdmax_2 + Filter: ((a >= 19) AND (b >= 5)) +(5 rows) + +drop table mc2pdmax; +create table mc2pdmin (a int, b int) partition by range (a, b); +create table mc2pdmin_default partition of mc2pdmin default; +create table mc2pdmin1 partition of mc2pdmin for values from (13, minvalue) to (19, 5); +-- Ensure we only scan the default +explain (costs off) select * from mc2pdmin where a = 5 and b <= 7; + QUERY PLAN +--------------------------------------- + Seq Scan on mc2pdmin_default mc2pdmin + Filter: ((b <= 7) AND (a = 5)) +(2 rows) + +-- Ensure we don't scan the default when the MINVALUE bound results in no rows +-- matching in the default partition. +explain (costs off) select * from mc2pdmin where a = 13 and b <= 7; + QUERY PLAN +----------------------------------- + Seq Scan on mc2pdmin1 mc2pdmin + Filter: ((b <= 7) AND (a = 13)) +(2 rows) + +-- Ensure we do scan the default to get rows with a < 13. +explain (costs off) select * from mc2pdmin where a <= 13 and b <= 7; + QUERY PLAN +----------------------------------------------- + Append + -> Seq Scan on mc2pdmin1 mc2pdmin_1 + Filter: ((a <= 13) AND (b <= 7)) + -> Seq Scan on mc2pdmin_default mc2pdmin_2 + Filter: ((a <= 13) AND (b <= 7)) +(5 rows) + +drop table mc2pdmin; +create table mc2pinf (a int, b int) partition by range (a, b); +create table mc2pinf_default partition of mc2pinf default; +create table mc2pinf1 partition of mc2pinf for values from (0, 0) to (maxvalue, maxvalue); +create table mc2pinf2 partition of mc2pinf for values from (minvalue, minvalue) to (0, 0); +-- Ensure the default partition is still pruned when the first key is +-- unbounded +explain (costs off) select * from mc2pinf where a = 32 and b >= -7; + QUERY PLAN +----------------------------------------------- + Seq Scan on mc2pinf1 mc2pinf + Filter: ((b >= '-7'::integer) AND (a = 32)) +(2 rows) + +explain (costs off) select * from mc2pinf where a = -32 and b <= 7; + QUERY PLAN +----------------------------------------------- + Seq Scan on mc2pinf2 mc2pinf + Filter: ((b <= 7) AND (a = '-32'::integer)) +(2 rows) + +drop table mc2pinf; -- boolean partitioning create table boolpart (a bool) partition by list (a); create table boolpart_default partition of boolpart default; diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql index 0a9d7391f13..85490aa6ce4 100644 --- a/src/test/regress/sql/partition_prune.sql +++ b/src/test/regress/sql/partition_prune.sql @@ -193,6 +193,53 @@ explain (costs off) select count(*) from mc2bp where c1 > 7; drop table mc2bp; +-- Ensure the default partition is not pruned when a bound is unbounded only +-- in a trailing key +create table mc2pdmax (a int, b int) partition by range (a, b); +create table mc2pdmax_default partition of mc2pdmax default; +create table mc2pdmax1 partition of mc2pdmax for values from (13, 0) to (19, maxvalue); + +-- Ensure we only scan the default partition in the following two cases +explain (costs off) select * from mc2pdmax where a = 32 and b >= -7; +explain (costs off) select * from mc2pdmax where a >= 25 and b >= 0; + +-- Ensure we don't scan the default when the MAXVALUE bound makes it so +-- no rows matching the WHERE clause can exist there. +explain (costs off) select * from mc2pdmax where a = 19 and b >= 5; + +-- Ensure we do scan the default to get the values with a > 19. +explain (costs off) select * from mc2pdmax where a >= 19 and b >= 5; + +drop table mc2pdmax; + +create table mc2pdmin (a int, b int) partition by range (a, b); +create table mc2pdmin_default partition of mc2pdmin default; +create table mc2pdmin1 partition of mc2pdmin for values from (13, minvalue) to (19, 5); + +-- Ensure we only scan the default +explain (costs off) select * from mc2pdmin where a = 5 and b <= 7; + +-- Ensure we don't scan the default when the MINVALUE bound results in no rows +-- matching in the default partition. +explain (costs off) select * from mc2pdmin where a = 13 and b <= 7; + +-- Ensure we do scan the default to get rows with a < 13. +explain (costs off) select * from mc2pdmin where a <= 13 and b <= 7; + +drop table mc2pdmin; + +create table mc2pinf (a int, b int) partition by range (a, b); +create table mc2pinf_default partition of mc2pinf default; +create table mc2pinf1 partition of mc2pinf for values from (0, 0) to (maxvalue, maxvalue); +create table mc2pinf2 partition of mc2pinf for values from (minvalue, minvalue) to (0, 0); + +-- Ensure the default partition is still pruned when the first key is +-- unbounded +explain (costs off) select * from mc2pinf where a = 32 and b >= -7; +explain (costs off) select * from mc2pinf where a = -32 and b <= 7; + +drop table mc2pinf; + -- boolean partitioning create table boolpart (a bool) partition by list (a); create table boolpart_default partition of boolpart default;