| From: | Ewan Young <kdbase(dot)hack(at)gmail(dot)com> |
|---|---|
| To: | PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org> |
| Cc: | dgrowleyml(at)gmail(dot)com, Jacob Brazeal <jacob(dot)brazeal(at)gmail(dot)com>, Tender Wang <tndrwang(at)gmail(dot)com>, amitlan(at)postgresql(dot)org |
| Subject: | RANGE partition pruning can still exclude the default partition |
| Date: | 2026-08-06 10:47:25 |
| Message-ID: | CAON2xHO=sqdqp=z8zWkybnWp0AuvefnAi2ez2vOYWrhXB6hHWQ@mail.gmail.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
Hi,
Issue still happens on master as of 9b917a93116, i.e. with 709dfd27f14
already applied. It is a different instance of the same defect, in another
branch of the same function, and it silently returns wrong results.
CREATE TABLE t (a int, b int) PARTITION BY RANGE (a, b);
CREATE TABLE t1 PARTITION OF t FOR VALUES FROM (13, 0) TO (19, MAXVALUE);
CREATE TABLE td PARTITION OF t DEFAULT;
INSERT INTO t VALUES (32, 5);
SELECT count(*) FROM t WHERE a = 32; -- 1, correct
SELECT count(*) FROM t WHERE a = 32 AND b >= -7; -- 0, should be 1
Adding a qual that is true for every matching row makes the row disappear;
the plan is a One-Time Filter: false. It takes a multi-column range key, a
bound unbounded in a trailing key but finite in the first key, and a query
constraining all key columns - with nvalues < partnatts,
get_matching_range_bounds() sets scan_default up front, which masks it.
DELETE and UPDATE silently skip rows as well, run-time pruning is affected,
and FROM (13, MINVALUE) is the mirror image. A layout that hits this in
the wild is RANGE (tenant_id, ts) with FROM (N, MINVALUE) TO (N, MAXVALUE)
per tenant plus a default partition.
At the end of get_matching_range_bounds() two adjustments drop the extreme
bound offset when no partition covers the key space beyond it:
int lastkey = nvalues - 1;
if (boundinfo->kind[maxoff - 1][lastkey] == PARTITION_RANGE_DATUM_MAXVALUE)
maxoff--;
Only a bound unbounded in its *first* key has no key space beyond it.
TO (19, MAXVALUE) merely means "unbounded within a = 19"; above it is real
key space owned by the default partition. Dropping the offset also drops
the last signal that the default has to be scanned: since 489247b0e615
get_matching_partitions() derives that from a returned offset whose
partindices[] entry is -1, and scan_default is not set on this path.
709dfd27f14 removed the equivalent adjustment from the nvalues == 0 path
for exactly this reason. These two blocks predate it - 9fdb675fc5d2 for
the surrounding code, 489247b0e615 for the kind[] tests - and are
byte-identical from REL_13_STABLE through master. I found no prior report
of the multi-column case; the 709dfd27f14 thread covers only the
IS NOT NULL / combine-step interaction, and the 2019 and 2018 default-
pruning threads go the other way.
0001 tests the first key of the bound instead of the last key of the lookup
value; the code change is two lines. I did not delete the blocks the way
709dfd27f14 did, because when the first key really is unbounded there is
nothing beyond it and removing them would scan the default partition for no
reason. Tests cover a trailing MAXVALUE, the MINVALUE mirror, a genuinely
unbounded first key, and the no-default case; the last two are unchanged by
the patch, on purpose.
0001 alone gives up one pruning opportunity: "WHERE a = 19 AND b >= 5" now
also scans the default partition. That pruning was unsound where it
happened - "WHERE a >= 19 AND b >= 5" makes the identical call (strategy
>=, values (19, 5)), and there the rows above the bound do qualify; master
answers it with 2 rows where 3 is correct. The function cannot tell the
two apart.
0002 recovers it where the knowledge exists. In the equality-prefix path
the code already skips the offset below the smallest matching bound when
that bound is (prefix, MINVALUE); the mirror was missing, so a greatest
matching bound of (prefix, MAXVALUE) still pulled in the offset above it.
With 0002, "WHERE a = 19 AND b >= 5" scans t1 only, while "a >= 19 AND
b >= 5" and plain "a = 19" still scan the default partition, the latter
because a NULL in a later key is routed there. 0002 is a pruning
improvement rather than a correctness fix, hence the split.
make check passes 245/245 with 0001 alone and with both applied, with no
changes to existing expected output; src/test/recovery and
src/test/subscription pass as well.
--
Regards,
Ewan Young
| Attachment | Content-Type | Size |
|---|---|---|
| v1-0001-Don-t-prune-the-default-partition-for-bounds-unbound.patch | application/octet-stream | 11.7 KB |
| v1-0002-Prune-the-default-partition-for-an-equality-prefix-e.patch | application/octet-stream | 4.7 KB |
| From | Date | Subject | |
|---|---|---|---|
| Previous Message | John Naylor | 2026-08-06 10:46:39 | Re: [PATCH] Use ssup_datum_*_cmp for int2, oid, and oid8 sort support |