Re: assertion failure with unique index + partitioning + join

Tender Wang <tndrwang@gmail.com>

From: Tender Wang <tndrwang@gmail.com>
To: PostgreSQL Hackers <pgsql-hackers@postgresql.org>
Cc: Alexander Kuzmenkov <akuzmenkov@tigerdata.com>
Date: 2026-06-16T01:58:07Z
Lists: pgsql-hackers
Alexander Kuzmenkov <akuzmenkov@tigerdata.com> 于2026年6月15日周一 23:39写道:
>
> Hi hackers,
>
> While I was running some random testing on the TimescaleDB extension, I noticed that I can easily hit an assertion failure in plain Postgres REL_18_STABLE. I haven't analyzed it at all, but thought I should report this:
>
> CREATE TABLE t (a bool UNIQUE) PARTITION BY LIST (a);
> CREATE TABLE p PARTITION OF t DEFAULT;
>                                                                                     SELECT
> FROM (
>     SELECT *
>     FROM t
>     LEFT JOIN t r USING (a)
> )
> WHERE a
> GROUP BY ();
>
>
> TRAP: failed Assert("bms_is_member(i, root->outer_join_rels)"), File: "../pg/src/backend/optimizer/path/equivclass.c", Line: 3631, PID: 1813021
>

I can reproduce this issue on HEAD.
The root->outer_join_rels is empty, and the i is 3(rel: t r).
The left join can be removed in remove_useless_joins(), in which
root->outer_join_rels is changed to be empty.
But the from->qual in this case has already been distributed to its rel.
(gdb) pgprint brel->baserestrictinfo
RestrictInfo [is_pushed_down=true can_join=false pseudoconstant=false
has_clone=false is_clone=false leakproof=false
has_volatile=VOLATILITY_UNKNOWN security_level=0
              num_base_rels=1 rinfo_serial=1 eval_cost={startup = -1,
per_tuple = 0} norm_selec=-1 outer_selec=-1 outer_is_left=false
hashjoinoperator=0 left_bucketsize=-1
              right_bucketsize=-1 left_mcvfreq=-1 right_mcvfreq=-1
left_hasheqoperator=0 right_hasheqoperator=0]
    [clause]
        PlaceHolderVar [phid=1 phlevelsup=0]
            [phexpr] Var [varno=2 varattno=1 vartype=16
varreturningtype=VAR_RETURNING_DEFAULT varnosyn=2 varattnosyn=1]
            [phrels] Bitmapset [4 3 2]
    [clause_relids] Bitmapset [2]
    [required_relids] Bitmapset [2]

It was distributed to (rel:2, table t), but I think "[phrels]
Bitmapset [4 3 2]" is incorrect. After leftjoin is removed, we no
longer need rel=4 (outerjoin) and rel=3 (t r).
But the qual has already been distributed to the (rel: 2) when
deconstruct_jointree (). Then the qual is inherited by its child(table
p)
It seems that we have no logic to process this distributed
restrictinfo in remove_useless_joins().

I need to dig deeper to find out how to fix it.


-- 
Thanks,
Tender Wang



Commits

  1. Strip removed-relation references from PlaceHolderVars at join removal

  2. Clean up remove_rel_from_query() after self-join elimination commit

  3. Fix incorrect handling of subquery pullup