<!--

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=fc069a3a6319b5bf40d2f0f1efceae1c9b7a68a8
The Self-Join Elimination (SJE) feature removes an inner join of a plain
table to itself in the query tree if it is proven that the join can be
replaced with a scan without impacting the query result.  Self-join and
inner relation get replaced with the outer in query, equivalence classes,
and planner info structures.  Also, the inner restrictlist moves to the
outer one with the removal of duplicated clauses.  Thus, this optimization
reduces the length of the range table list (this especially makes sense for
partitioned relations), reduces the number of restriction clauses and,
in turn, selectivity estimations, and potentially improves total planner
prediction for the query.

This feature is dedicated to avoiding redundancy, which can appear after
pull-up transformations or the creation of an EquivalenceClass-derived clause
like the below.

  SELECT * FROM t1 WHERE x IN (SELECT t3.x FROM t1 t3);
  SELECT * FROM t1 WHERE EXISTS (SELECT t3.x FROM t1 t3 WHERE t3.x = t1.x);
  SELECT * FROM t1,t2, t1 t3 WHERE t1.x = t2.x AND t2.x = t3.x;

Also, it can drastically help to join partitioned tables, removing entries
even before their expansion.


We can remove a self-join when for each outer row:
 1. At most, one inner row matches the join clause;
 2. Each matched inner row must be (physically) the same as the outer one;
 3. Inner and outer rows have the same row mark.


Gleu : 

https://blog.dalibo.com/2025/03/07/postgresql-18-self_join_elimination.html

-->

<div class="slide-content">

 * Jointure d'une table avec elle-même
   + automatiquement supprimée
 * Cette problématique arrive facilement 
 * 6 ans de discussion pour le patch !

</div>

<div class="notes">

Il semble absurde de joindre une table avec elle-même
sur sa clé primaire, mais cela arrive plus souvent que l'on ne croit,
surtout involontairement.

Il n'est pas rare que des utilisateurs n'aient accès qu'à
des vues « de présentation », et qu'ils ajoutent une jointure
sur une table déjà présente dans cette vue pour ajouter un champ manquant.
Il arrive même qu'on le fasse en connaissance de cause quand le schéma
n'est pas aisément modifiable
C'est parfois en connaissance quand n'est pas aisément modifiable
(pas les droits, pas le temps de valider, pas de contrôle sur l'ORM,
la vue est fournie par un éditeur, etc.)

PostgreSQL sait à présent repérer ce cas et supprime une auto-jointure
quand il peut prouver que pour chaque ligne :

  * il y a au plus une ligne en face ;
  * les deux lignes des deux côtés de la jointure seraient physiquement les mêmes
  (concrètement : même `ctid`) ; <!-- https://www.postgresql.org/message-id/552e481b-2feb-75fd-4e9f-4199bfd1c1f3%40postgrespro.ru -->
<!-- FIXME   * 3. Inner and outer rows have the same row mark. -->

Seules les tables, et avec une clé unique ou primaire,
peuvent être optimisées. L'optimisation ne fonctionne pas pour les vues
matérialisées, par exemple.

<!-- Ceci juste pour donner un aperçu du dev -->
PostgreSQL 9.0 savait déjà éliminer des _left joins_ inutiles.
Pour la _self-join elimination_, plus complexe qu'elle n'en a l'air,
il aura fallu une
[vingtaine de contributeurs expérimentés](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=fc069a3a6319b5bf40d2f0f1efceae1c9b7a68a8)
et presque
[sept ans de discussions](https://www.postgresql.org/message-id/flat/64486b0b-0404-e39e-322d-0801154901f3%40postgrespro.ru) !
Une première version avait même été intégrée à PostgreSQL 17
mais retirée avant sa parution à cause de bugs.
Parmi les problèmes rencontrés :
la complexité du code de l'optimiseur,
l'habituelle crainte que la recherche de l'optimisation coûte cher
en temps de planification pour toutes les requêtes,
la quantité assez importante de code nécessaire pour un cas rare, <!-- Heikki -->
l'endroit exact où placer l'optimisation,
l'implémentation et l'optimisation du code,
l'inhibition du traitement des _self-joins_ lors d'`UPDATE`,
ou avec `TABLESAMPLE`…,
la recherche de cas tordus,
un changement d'auteur en cours de route,
l'impact sur l'extension `pg_hint_plan`…

Un paramètre `enable_self_join_elimination` apparaît pour inhiber
la fonctionnalité, ce qui ne devrait pas être nécessaire.

**Exemple** :

Partons d'une base générée par `pgbench`.
Chaque intervenant utilise une vue pour rajouter une information
qui lui manque dans la vue précédente :

```sql
DROP VIEW IF EXISTS pgbench_accounts_balance_v CASCADE ;

-- Première vue montrant juste l'ID et le compte courant
CREATE VIEW pgbench_accounts_balance_v
AS
SELECT aid, abalance
FROM pgbench_accounts a1 ;

-- L'utilisateur suivant ajoute le champ texte
CREATE VIEW pgbench_accounts_v2
AS
SELECT v.*, a2.filler AS libelle
FROM pgbench_accounts a2
INNER JOIN pgbench_accounts_balance_v v USING (aid) ;

-- On a besoin des infos sur la branche (pgbench_branches)
-- mais il faut trouver la clé dans pgbench_accounts
CREATE VIEW pgbench_accounts_v3
AS
SELECT v2.aid, v2.abalance, a3.bid, b3.bbalance
FROM pgbench_accounts_v2 v2
INNER JOIN pgbench_accounts a3 USING (aid)
INNER JOIN pgbench_branches b3 ON (a3.bid = b3.bid) ;

-- On re-rajoute le libellé oublié dans la vue précédente
-- et on teste que la ligne fait partie des lignes valides
CREATE VIEW pgbench_accounts_v4
AS
SELECT v3.*, a4.filler AS libelle_bis
FROM pgbench_accounts_v3 v3
INNER JOIN pgbench_accounts a4 USING (aid)
WHERE EXISTS (
     SELECT 'existe' FROM pgbench_accounts x
     WHERE filler IS NOT NULL
     AND x.aid = a4.aid
) ;

-- L'utilisateur rajoute le libellé côté branche
-- plus divers tests
EXPLAIN
SELECT v4.*, b5.filler AS libelle_branche
FROM pgbench_accounts_v4 v4
INNER JOIN pgbench_branches b5 USING (bid)
WHERE v4.aid IS NOT NULL AND v4.bbalance > 0 AND libelle_bis IS NOT NULL ;
```

Le plan de cette dernière requête avec PostgreSQL 18
ne comprend qu'une simple jointure par _hash join_
entre les deux tables concernées,
avec les divers filtres, les redondances en moins.
Même le `EXISTS` est interprété comme une jointure et donc
éliminé aussi.

```output
 Hash Join  (cost=2.26..291299.76 rows=100000 width=457)
   Hash Cond: (x.bid = b3.bid)
   ->  Seq Scan on pgbench_accounts x  (cost=0.00..263935.00 rows=10000000 width=97)
         Filter: (filler IS NOT NULL)
   ->  Hash  (cost=2.25..2.25 rows=1 width=364)
         ->  Seq Scan on pgbench_branches b3  (cost=0.00..2.25 rows=1 width=364)
               Filter: (bbalance > 0)
```
Comparons avec le plan sous PostgreSQL 17 :
il [contient 18 nœuds](https://explain.dalibo.com/plan/70699d4d5fdcd676),
dont quatre _Seq Scan_ et un _Index Scan_ sur `pgbench_accounts`,
et deux _Seq Scan_ sur `pgbench_branches` !

Cette fonctionnalité est très pratique mais n'est pas infaillible.
La requête suivante ajoute un filtre sur `aid` qui est propagé
sur toutes les jointures, ce qui désactive
la _self-join elimination_.
```sql
EXPLAIN (COSTS OFF)
SELECT * FROM pgbench_accounts_v4
WHERE libelle_bis IS NOT NULL
AND aid = 10000 ;
```
```output
Nested Loop Semi Join
->  Nested Loop
        ->  Nested Loop
            ->  Nested Loop
                    ->  Hash Join
                        Hash Cond: (b3.bid = a3.bid)
                        ->  Seq Scan on pgbench_branches b3
                        ->  Hash
                                ->  Index Scan using pgbench_accounts_pkey on pgbench_accounts a3
                                    Index Cond: (aid = 10000)
                    ->  Index Only Scan using pgbench_accounts_pkey on pgbench_accounts a2
                        Index Cond: (aid = 10000)
            ->  Index Scan using pgbench_accounts_pkey on pgbench_accounts a1
                    Index Cond: (aid = 10000)
        ->  Index Scan using pgbench_accounts_pkey on pgbench_accounts a4
            Index Cond: (aid = 10000)
            Filter: (filler IS NOT NULL)
->  Index Scan using pgbench_accounts_pkey on pgbench_accounts x
        Index Cond: (aid = 10000)
        Filter: (filler IS NOT NULL)
```
Ce n'est pas ici un souci, car l'accès par les index
est alors réellement rapide.


Voir aussi cet article de Guillaume Lelarge sur le blog Dalibo :
[PostgreSQL 18 - Suppression de jointures inutiles](https://blog.dalibo.com/2025/03/07/postgresql-18-self_join_elimination.html).

</div>
