Wrong results from self-join elimination with different restriction operators

From: Tender Wang <tndrwang(at)gmail(dot)com>
To: PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Cc: Alexander Korotkov <aekorotkov(at)gmail(dot)com>
Subject: Wrong results from self-join elimination with different restriction operators
Date: 2026-10-12 06:03:50
Message-ID: CAHewXNmwYrp0DTuisCyfXXjVa_irUdDFApUzLVDg3J4MAR6gEg@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

After finding the SJE bug involving inheritance scans and ONLY, I
asked an LLM to generate fuzz tests to look for further issues in
self-join elimination.
This uncovered another wrong-result case involving restriction clauses
with different equality operators.

How to reproduce:
...
postgres=# CREATE FUNCTION text_ci_eq(text, text) RETURNS boolean
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE
AS $$
BEGIN
RETURN lower($1) = lower($2);
END $$;
CREATE FUNCTION text_ci_cmp(text, text) RETURNS int
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
AS $$SELECT CASE WHEN lower($1) < lower($2) THEN -1
WHEN lower($1) > lower($2) THEN 1 ELSE 0 END$$;
CREATE OPERATOR === (LEFTARG=text, RIGHTARG=text, PROCEDURE=text_ci_eq,
COMMUTATOR='===', RESTRICT=eqsel, JOIN=eqjoinsel, MERGES);
CREATE OPERATOR FAMILY ci_text_fam USING btree;
CREATE OPERATOR CLASS ci_text_ops FOR TYPE text USING btree FAMILY
ci_text_fam AS
OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 ===, OPERATOR 4 >=, OPERATOR 5 >,
FUNCTION 1 text_ci_cmp;

CREATE TABLE ci_t(id int, a text, b int);
CREATE UNIQUE INDEX ci_t_a ON ci_t(a); -- deterministic (standard)
unique index
INSERT INTO ci_t VALUES (1,'a',1),(2,'A',1),(3,'b',2);
ANALYZE ci_t;

SET enable_self_join_elimination = on;
SELECT p1.id, p1.a, p2.id, p2.a
FROM ci_t p1 JOIN ci_t p2 ON p1.b = p2.b
WHERE p1.a === 'a' AND p2.a = 'a'
ORDER BY 1, 3;
id | a | id | a
----+---+----+---
1 | a | 1 | a
(1 row)

SET enable_self_join_elimination = off;
SELECT p1.id, p1.a, p2.id, p2.a
FROM ci_t p1 JOIN ci_t p2 ON p1.b = p2.b
WHERE p1.a === 'a' AND p2.a = 'a'
ORDER BY 1, 3;
id | a | id | a
----+---+----+---
1 | a | 1 | a
2 | A | 1 | a
(2 rows)
...

SJE incorrectly considers the two restrictions equivalent and
eliminates the join.

The unique index and p2.a = 'a' establish uniqueness on the kept side.
match_unique_clauses() then checks for a corresponding restriction on
the removed side, but compares only the operands, ignoring the
operator and input collation.
It therefore treats p1.a === 'a' as matching p2.a = 'a', even though
they select different rows.

The attached patch requires both the operator and input collation to
match, in addition to the existing operand checks.
It includes a regression test for the different-operator case and
verifies that identical restrictions still allow self-join
elimination.

The full reproducer, including the custom operator definitions, is in
the regression test.

--
Thanks,
Tender Wang

Attachment Content-Type Size
0001-Fix-restriction-matching-in-self-join-elimination.patch text/plain 10.3 KB

Browse pgsql-hackers by date

  From Date Subject
Next Message Jakub Wartak 2026-10-12 06:08:47 Re: pg_*_advice: tsv load failure, etc.
Previous Message shveta malik 2026-10-12 06:01:30 Re: Proposal: Conflict log history table for Logical Replication