| From: | Tender Wang <tndrwang(at)gmail(dot)com> |
|---|---|
| To: | Jacob Brazeal <jacob(dot)brazeal(at)gmail(dot)com> |
| Cc: | PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org> |
| Subject: | Re: Partition pruning can incorrectly exclude a RANGE default partition |
| Date: | 2026-07-27 09:48:22 |
| Message-ID: | CAHewXNk1Xh_Eo7RAs8Yqr-LkS0LW7APbX66mCcqZ_y3sLUXXNw@mail.gmail.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
Jacob Brazeal <jacob(dot)brazeal(at)gmail(dot)com> 于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
| Attachment | Content-Type | Size |
|---|---|---|
| 0001-Fix-partition_prue.patch | application/octet-stream | 954 bytes |
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Heikki Linnakangas | 2026-07-27 10:18:09 | Re: [PATCH] Speed up repeat() for larger counts |
| Previous Message | Andy Fan | 2026-07-27 09:17:44 | Re: Tracking per-RelOptInfo uniqueness during planning |