From 4704cb616434dad992703c9a2fc641ef0a362dcd Mon Sep 17 00:00:00 2001 From: David Rowley Date: Tue, 8 Sep 2026 13:32:31 +1200 Subject: [PATCH v3] 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 --- src/backend/partitioning/partprune.c | 62 +++++---- src/test/regress/expected/partition_prune.out | 126 ++++++++++++++++++ src/test/regress/sql/partition_prune.sql | 52 ++++++++ 3 files changed, 214 insertions(+), 26 deletions(-) diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c index 5e98f503244..14d88bc79e7 100644 --- a/src/backend/partitioning/partprune.c +++ b/src/backend/partitioning/partprune.c @@ -3143,9 +3143,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); @@ -3336,43 +3344,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. + * 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 (minoff < boundinfo->ndatums && partindices[minoff] < 0) + if (nvalues == 1) { - int lastkey = nvalues - 1; - - if (boundinfo->kind[minoff][lastkey] == - PARTITION_RANGE_DATUM_MINVALUE) + /* + * 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 8f1119a026d..baf6ad5ced9 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -1132,6 +1132,132 @@ 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); +insert into mc2pdmax values (32, 5); +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) + +select * from mc2pdmax where a = 32 and b >= -7; + a | b +----+--- + 32 | 5 +(1 row) + +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) + +select * from mc2pdmax where a >= 25 and b >= 0; + a | b +----+--- + 32 | 5 +(1 row) + +-- The same lookup value reaches the key space above the bound here but not in +-- the a = 19 case below, so the default partition must be scanned for the +-- first query only. +insert into mc2pdmax values (19, 5); +select * from mc2pdmax where a >= 19 and b >= 5; + a | b +----+--- + 19 | 5 + 32 | 5 +(2 rows) + +select * from mc2pdmax where a = 19 and b >= 5; + a | b +----+--- + 19 | 5 +(1 row) + +-- With the prefix pinned to the bound's first key, nothing carrying that +-- prefix can sort above (19, maxvalue), so the default partition is not needed +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) + +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); +insert into mc2pdmin values (5, 5); +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) + +select * from mc2pdmin where a = 5 and b <= 7; + a | b +---+--- + 5 | 5 +(1 row) + +drop table mc2pdmin; +-- Ensure the default partition is still pruned when the first key is unbounded +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); +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; +-- Ensure the uncovered key space matches nothing when there is no default +create table mc2pnodef (a int, b int) partition by range (a, b); +create table mc2pnodef1 partition of mc2pnodef for values from (13, 0) to (19, maxvalue); +explain (costs off) select * from mc2pnodef where a = 32 and b >= -7; + QUERY PLAN +------------------------------- + Result + Replaces: Scan on mc2pnodef + One-Time Filter: false +(3 rows) + +explain (costs off) select * from mc2pnodef where a = 15 and b >= 0; + QUERY PLAN +----------------------------------- + Seq Scan on mc2pnodef1 mc2pnodef + Filter: ((b >= 0) AND (a = 15)) +(2 rows) + +drop table mc2pnodef; -- 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 f967658d4b5..9fc11ea569e 100644 --- a/src/test/regress/sql/partition_prune.sql +++ b/src/test/regress/sql/partition_prune.sql @@ -212,6 +212,58 @@ 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); +insert into mc2pdmax values (32, 5); + +explain (costs off) select * from mc2pdmax where a = 32 and b >= -7; +select * from mc2pdmax where a = 32 and b >= -7; +explain (costs off) select * from mc2pdmax where a >= 25 and b >= 0; +select * from mc2pdmax where a >= 25 and b >= 0; + +-- The same lookup value reaches the key space above the bound here but not in +-- the a = 19 case below, so the default partition must be scanned for the +-- first query only. +insert into mc2pdmax values (19, 5); +select * from mc2pdmax where a >= 19 and b >= 5; +select * from mc2pdmax where a = 19 and b >= 5; + +-- With the prefix pinned to the bound's first key, nothing carrying that +-- prefix can sort above (19, maxvalue), so the default partition is not needed +explain (costs off) select * from mc2pdmax where a = 19 and b >= 5; +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); +insert into mc2pdmin values (5, 5); + +explain (costs off) select * from mc2pdmin where a = 5 and b <= 7; +select * from mc2pdmin where a = 5 and b <= 7; +drop table mc2pdmin; + +-- Ensure the default partition is still pruned when the first key is unbounded +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); + +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; + +-- Ensure the uncovered key space matches nothing when there is no default +create table mc2pnodef (a int, b int) partition by range (a, b); +create table mc2pnodef1 partition of mc2pnodef for values from (13, 0) to (19, maxvalue); + +explain (costs off) select * from mc2pnodef where a = 32 and b >= -7; +explain (costs off) select * from mc2pnodef where a = 15 and b >= 0; +drop table mc2pnodef; + -- boolean partitioning create table boolpart (a bool) partition by list (a); create table boolpart_default partition of boolpart default; -- 2.53.0