Jacob Brazeal <[email protected]> 于2026年7月26日周日 11:14写道: > > Hi hackers, > > I found a partition-pruning bug where adding a redundant IS NOT NULL > condition can cause a RANGE table’s default partition to be pruned, producing > wrong results. > > This reproduces on current master (13b7a8a0ef5): > > CREATE TABLE s2 (a int) PARTITION BY RANGE (a); > CREATE TABLE s2_1 PARTITION OF s2 FOR VALUES FROM (0) TO (10); > CREATE TABLE s2_d PARTITION OF s2 DEFAULT; > > INSERT INTO s2 VALUES (5), (15); > > SELECT count(*) FROM s2 > WHERE a IN (5, 15); > > SELECT count(*) FROM s2 > WHERE a IS NOT NULL > AND a IN (5, 15); > > The first query correctly returns 2. The second returns 1, although a IS NOT > NULL is true for both rows. > > The plan for the wrong query contains only the ordinary partition: > > Seq Scan on s2_1 s2 > Filter: ((a IS NOT NULL) AND > (a = ANY ('{5,15}'::integer[]))) > > The default partition is absent. Disabling partition pruning, or querying the > two leaf partitions with UNION ALL, returns the correct two rows. > > The problem also occurs with equivalent forms that generate multiple pruning > steps, for example: > > a IS NOT NULL AND a = ANY (ARRAY[5,15]) > a IS NOT NULL AND (a = 5 OR a = 15) > a IS NOT NULL AND (a < 6 OR a > 14) > > A simple equality does not reproduce: > > WHERE a IS NOT NULL AND a = 15 > > Run-time pruning is affected as well: > > SET plan_cache_mode = force_generic_plan; > > PREPARE s2p(int, int) AS > SELECT count(*) FROM s2 > WHERE a IS NOT NULL > AND a IN ($1, $2); > > EXECUTE s2p(5, 15); > > This also returns 1, with the default subplan removed. > > The issue appears to be in the nvalues == 0 path of > get_matching_range_bounds(). That path removes bound offsets whose > partindices[] entry is -1 and records the default partition through > scan_default instead. > > For this query, the IS NOT NULL pruning step therefore represents the > default partition using scan_default, while the IN step represents it > using a -1 bound offset. The INTERSECT combine step treats those as > independent representations: > > result->bound_offsets = > bms_int_members(result->bound_offsets, > step_result->bound_offsets); > > if (result->scan_default) > result->scan_default = step_result->scan_default; > > The default offset is removed by the bitmap intersection, and > scan_default is then cleared, so the default partition is lost. >
Yes, it seems that we should use step_result instead of result in
perform_pruning_combine_step().
After replacing it, I get the correct result.
diff --git a/src/backend/partitioning/partprune.c
b/src/backend/partitioning/partprune.c
index e7c318bbcac..8eedb6cce16 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -3685,9 +3685,9 @@
perform_pruning_combine_step(PartitionPruneContext *context,
step_result->bound_offsets);
/* Update whether to scan null
and default partitions. */
- if (result->scan_null)
+ if (step_result->scan_null)
result->scan_null =
step_result->scan_null;
- if (result->scan_default)
+ if (step_result->scan_default)
result->scan_default =
step_result->scan_default;
postgres=# SELECT count(*) FROM s2
WHERE a IS NOT NULL
AND a IN (5, 15);
count
-------
2
(1 row)
postgres=# SET plan_cache_mode = force_generic_plan;
SET
postgres=# PREPARE s2p(int, int) AS
SELECT count(*) FROM s2
WHERE a IS NOT NULL
AND a IN ($1, $2);
PREPARE
postgres=# execute s2p(5,15);
count
-------
2
(1 row)
See the attached patch. (no test ).
--
Thanks,
Tender Wang
0001-Fix-partition_prue.patch
Description: Binary data
