| From: | Jacob Brazeal <jacob(dot)brazeal(at)gmail(dot)com> |
|---|---|
| To: | PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org> |
| Subject: | Partition pruning can incorrectly exclude a RANGE default partition |
| Date: | 2026-07-26 03:13:51 |
| Message-ID: | CA+COZaDXrfTaBjLE=Z79MTaH6Xun1V4PeKxLvCNv8mXS8wn0rw@mail.gmail.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
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.
Best,
Jacob Brazeal
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Chengpeng Yan | 2026-07-26 03:15:56 | Re: Extended statistics improvement: multi-column MCV missing values |
| Previous Message | Tom Lane | 2026-07-26 02:15:28 | Re: remove_useless_joins vs. bug #19560 |