Re: Tracking per-RelOptInfo uniqueness during planning

From: Antonin Houska <ah(at)cybertec(dot)at>
To: Richard Guo <guofenglinux(at)gmail(dot)com>
Cc: Ilia Evdokimov <ilya(dot)evdokimov(at)tantorlabs(dot)com>, Pg Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>, Andy Fan <zhihuifan1213(at)163(dot)com>, David Rowley <dgrowleyml(at)gmail(dot)com>
Subject: Re: Tracking per-RelOptInfo uniqueness during planning
Date: 2026-08-25 13:59:03
Message-ID: 22942.1787666343@localhost
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Richard Guo <guofenglinux(at)gmail(dot)com> wrote:

> Thanks. Fixed in v4. v4 also drops the costsize.c changes from 0003,
> as the costing issue has been fixed on master.

A few more comments:

* populate_joinrel_uniquekeys()

Shouldn't we care about the number of unique keys created by the combination
technique? If most / all relations do have the unique keys, the list of keys
can grow exponentially as we keep adding relations to the join.

* uniquekeys_match_join_clauses()

/*
* Nullable keys are fine here: rows with NULL key values cannot match any
* outer row through these strict clauses.
*/

I'm not able to find where the callers check the strictness of the clauses.

Also, "Nullable" => "null-unaware" ?

* strengthen_uniquekeys_for_join()

In the header comment:

"The caller must have established that every clause holds of every output" =>
"...holds for ..." ?

I also tried to adjust one comment regarding "strengthening" of the unique
keys - see the attached diff. The original wording was hard form me to
understand, so I elaborated it more in detail. (Of course it's possible that I
missed the point altogether.)

Comments on regression tests:

v4-0001
-------

* The following

+-- a key column equated to a constant drops out of the key
+explain (costs off) select distinct b from uk_pk2 where a = 5;
+ QUERY PLAN
+----------------------------------------
+ Bitmap Heap Scan on uk_pk2
+ Recheck Cond: (a = 5)
+ -> Bitmap Index Scan on uk_pk2_pkey
+ Index Cond: (a = 5)
+(4 rows)

does not seem prove what the comment claims because explicit grouping is (for
different reason, probably due to the unique key not being a subset of the
DISTINCT clause) needed even without the WHERE clause:

explain (costs off) select distinct b from uk_pk2;
QUERY PLAN
--------------------------
HashAggregate
Group Key: b
-> Seq Scan on uk_pk2
(3 rows)

* To test the translation of the unique keys into the outer query,

+--
+-- Subquery-in-FROM keys
+--
+-- a DISTINCT inside is translated into the outer query
+explain (costs off) select distinct a, b from (select distinct a, b from uk_pk2) s;
+ QUERY PLAN
+--------------------
+ Seq Scan on uk_pk2
+(1 row)
+
+-- likewise a GROUP BY
+explain (costs off) select distinct a, b from (select a, b from uk_pk2 group by a, b) s;
+ QUERY PLAN
+--------------------
+ Seq Scan on uk_pk2
+(1 row)

I think neither DISTINCT nor GROUP BY is needed in the subquery. The subquery should have the appropriate unique keys anyway:

explain (costs off) select distinct a, b from (select a, b from uk_pk2) s;
QUERY PLAN
--------------------
Seq Scan on uk_pk2
(1 row)

* What's the point of the OFFSET clause here?

+-- a key deduced inside the subquery's own join search
+explain (costs off)
+select distinct x from
+ (select uk_p.id as x from uk_p join uk_q on uk_p.id = uk_q.id offset 0) s;
+ QUERY PLAN
+----------------------------------
+ Hash Join
+ Hash Cond: (uk_p.id = uk_q.id)
+ -> Seq Scan on uk_p
+ -> Hash
+ -> Seq Scan on uk_q
+(5 rows)

I'm getting the same plan w/o that.

* I was wondering what's specific about CROSS JOIN (v03-0001)

+-- one side's key alone is not a key of a cross join
+explain (costs off) select distinct uk_p.id from uk_p cross join uk_q;
+ QUERY PLAN
+-----------------------------------------------------
+ Unique
+ -> Nested Loop
+ -> Index Only Scan using uk_p_pkey on uk_p
+ -> Materialize
+ -> Seq Scan on uk_q
+(5 rows)

but debugger session revealed that populate_plain_rel_uniquekeys() does not
find the ECs for uk_q. I think it's because the table is not mentioned in the
DISTINCT clause. Once I added it, the appropriate unique key seems to exist,
even when the join is CROSS JOIN:

explain (costs off) select distinct uk_p.id, uk_q.id from uk_p cross join uk_q;
QUERY PLAN
------------------------------
Nested Loop
-> Seq Scan on uk_p
-> Materialize
-> Seq Scan on uk_q
(4 rows)

* Regarding FULL JOIN

+-- a full join null-extends both sides, so nothing survives
+explain (costs off)
+select distinct uk_p.id from uk_p full join uk_q on uk_p.id = uk_q.id;

don't we also need a query whith "DISTINCT uk_q.id" clause?

* I'm not sure the unique keys are tested here

+-- the RHS of this semijoin is already distinct, so it is not unique-ified
+explain (costs off)
+select * from uk_p, uk_q where (uk_p.id, uk_q.id) in (select a, b from uk_pk2);
+ QUERY PLAN
+-----------------------------------------
+ Hash Join
+ Hash Cond: (uk_pk2.b = uk_q.id)
+ -> Hash Join
+ Hash Cond: (uk_pk2.a = uk_p.id)
+ -> Seq Scan on uk_pk2
+ -> Hash
+ -> Seq Scan on uk_p
+ -> Hash
+ -> Seq Scan on uk_q
+(9 rows)

I'm getting the same plan even if I comment out the fast path (i.e. the use of
the unique keys) in rel_is_distinct_for().

* Is this comment

+-- across a left join the RHS key covers only the matched rows
+explain (costs off) select distinct uk_d1.u from uk_d2 left join uk_d1 on uk_d2.id = uk_d1.u;

saying that the RHS "loses its NULL-awareness", as tested earlier in the
script, or is it a different problem?

v4-0002
-------

* Maybe

"A partitioned table's unique index must include ..."
=>
"A partitioned table's unique index key must include ..."

* " ... an inheritance parent has none"

Does that mean " ... an inheritance parent has no unique indexes", because
inheritance parent is treated as if it had no indexes at all, per
get_relation_info()?

/*
* Make list of indexes. Ignore indexes on system catalogs if told to.
* Don't bother with indexes from traditional inheritance parents. For
* partitioned tables, we need a list of at least unique indexes as these
* serve as unique proofs for certain planner optimizations. However,
* let's not discriminate here and just record all partitioned indexes
* whether they're unique indexes or not.
*/

v4-0003
-------

* The patch seems to handle grouped child relation, so it'd make sense to have
a test for that.

--
Antonin Houska
Web: https://www.cybertec-postgresql.com

Attachment Content-Type Size
nocfbot.Attempt-to-reword-a-comment.patch text/x-diff 1.6 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Melanie Plageman 2026-08-25 14:06:24 Re: Allow aggressive VACUUM to freeze without a cleanup lock
Previous Message Peter Eisentraut 2026-08-25 13:52:08 Re: Vertex/Edge label and view