From bd50977684239bf5134e64d5514a60ea377ea234 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Tue, 25 Aug 2026 20:23:54 +1200 Subject: [PATCH v2] Fix incorrect multi-column RANGE partition pruning When performing partition pruning with a RANGE partitioned table where the pruning quals are only present for a leading prefix of the partition key and when using a <= operator, it was possible that partition pruning would accidentally prune away some partitions which shouldn't be pruned when those partitions matched the given qual. This happened due to an incorrectly coded loop bound which was checking the bound was within the required range. The loop failed to consider that the loop body would adjust the bound offset to a value within range. Here we fix this by moving the loop condition so we break out of the loop if the adjusted offset is not within the required range. p: partition by range (a, b); p1: for values from (1, 4) to (1, 7); p2: for values from (1, 7) to (3, 8); p3: for values from (4, 8) to (6, 9); def: default; select * from p where a <= 1; Here p2 was pruned by mistake. --- src/backend/partitioning/partprune.c | 13 ++++-- src/test/regress/expected/partition_prune.out | 45 +++++++++++++++++++ src/test/regress/sql/partition_prune.sql | 24 ++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c index 06566c8ce8a..87516792434 100644 --- a/src/backend/partitioning/partprune.c +++ b/src/backend/partitioning/partprune.c @@ -3206,12 +3206,14 @@ get_matching_range_bounds(PartitionPruneContext *context, * of smallest such bound) or find the smallest one that's * greater than the lookup values and set minoff to that. */ - while (off >= 1 && off < boundinfo->ndatums - 1) + while (true) { int32 cmpval; int nextoff; nextoff = inclusive ? off - 1 : off + 1; + if (nextoff < 0 || nextoff >= boundinfo->ndatums) + break; cmpval = partition_rbound_datum_cmp(partsupfunc, partcollation, @@ -3265,16 +3267,21 @@ get_matching_range_bounds(PartitionPruneContext *context, if (off >= 0) { /* - * See the comment above. + * As above, check adjacent bounds to see if the bound is + * equal to the lookup value. */ if (is_equal && nvalues < partnatts) { - while (off >= 1 && off < boundinfo->ndatums - 1) + while (true) { int32 cmpval; int nextoff; nextoff = inclusive ? off + 1 : off - 1; + + if (nextoff < 1 || nextoff >= boundinfo->ndatums - 1) + break; + cmpval = partition_rbound_datum_cmp(partsupfunc, partcollation, boundinfo->datums[nextoff], diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index aa821646011..71bd009d0d2 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -1099,6 +1099,26 @@ explain (costs off) select * from mc2p where b is null; Filter: (b IS NULL) (2 rows) +create table mc2ap (a int, b int) partition by range (a, b); +create table mc2ap1 partition of mc2ap for values from (1, 4) to (1, 7); +create table mc2ap2 partition of mc2ap for values from (1, 7) to (3, 8); +create table mc2ap3 partition of mc2ap for values from (4, 8) to (6, 9); +create table mc2ap_def partition of mc2ap default; +-- Ensure we scan all partitions apart from mc2ap3 +explain (costs off) select count(*) from mc2ap where a <= 1; + QUERY PLAN +------------------------------------------- + Aggregate + -> Append + -> Seq Scan on mc2ap1 mc2ap_1 + Filter: (a <= 1) + -> Seq Scan on mc2ap2 mc2ap_2 + Filter: (a <= 1) + -> Seq Scan on mc2ap_def mc2ap_3 + Filter: (a <= 1) +(8 rows) + +drop table mc2ap; -- boolean partitioning create table boolpart (a bool) partition by list (a); create table boolpart_default partition of boolpart default; @@ -5002,3 +5022,28 @@ select * from (select a, b from phv_boolpart) t (2 rows) drop table phv_boolpart; +-- Check prefix pruning when matching bounds reach the end of datums[]. +create table mc2p_edge (a int, b int) partition by range (a, b); +create table mc2p_edge1 partition of mc2p_edge + for values from (1, 10) to (1, 20); +create table mc2p_edge_default partition of mc2p_edge default; +-- only the default partition can contain rows with a > 1 +explain (costs off) select * from mc2p_edge where a > 1; + QUERY PLAN +----------------------------------------- + Seq Scan on mc2p_edge_default mc2p_edge + Filter: (a > 1) +(2 rows) + +-- both partitions may contain rows with a >= 1 +explain (costs off) select * from mc2p_edge where a >= 1; + QUERY PLAN +------------------------------------------------- + Append + -> Seq Scan on mc2p_edge1 mc2p_edge_1 + Filter: (a >= 1) + -> Seq Scan on mc2p_edge_default mc2p_edge_2 + Filter: (a >= 1) +(5 rows) + +drop table mc2p_edge; diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql index dac673ef80a..85fce32f86b 100644 --- a/src/test/regress/sql/partition_prune.sql +++ b/src/test/regress/sql/partition_prune.sql @@ -192,6 +192,17 @@ explain (costs off) select * from mc2p where a is null and b = 1; explain (costs off) select * from mc2p where a is null; explain (costs off) select * from mc2p where b is null; +create table mc2ap (a int, b int) partition by range (a, b); +create table mc2ap1 partition of mc2ap for values from (1, 4) to (1, 7); +create table mc2ap2 partition of mc2ap for values from (1, 7) to (3, 8); +create table mc2ap3 partition of mc2ap for values from (4, 8) to (6, 9); +create table mc2ap_def partition of mc2ap default; + +-- Ensure we scan all partitions apart from mc2ap3 +explain (costs off) select count(*) from mc2ap where a <= 1; + +drop table mc2ap; + -- boolean partitioning create table boolpart (a bool) partition by list (a); create table boolpart_default partition of boolpart default; @@ -1538,3 +1549,16 @@ select * from (select a, b from phv_boolpart) t group by grouping sets (a, b); drop table phv_boolpart; + +-- Check prefix pruning when matching bounds reach the end of datums[]. +create table mc2p_edge (a int, b int) partition by range (a, b); +create table mc2p_edge1 partition of mc2p_edge + for values from (1, 10) to (1, 20); +create table mc2p_edge_default partition of mc2p_edge default; + +-- only the default partition can contain rows with a > 1 +explain (costs off) select * from mc2p_edge where a > 1; +-- both partitions may contain rows with a >= 1 +explain (costs off) select * from mc2p_edge where a >= 1; +drop table mc2p_edge; + -- 2.43.0