From 274ce7f564faf51ba575a0199f19f3d0e8991652 Mon Sep 17 00:00:00 2001
From: David Rowley <dgrowley@gmail.com>
Date: Thu, 20 Aug 2026 12:43:35 +1200
Subject: [PATCH] Attempt to stabilize self-join test in tidscan.sql

This self-join test has been known to fail due to the join order
swapping, resulting in the EXPLAIN no longer matching the expected
output.  Currently, that only seems to happen in v14, as some efforts were
made in 74388a1ac and 4496020e6 to reduce the chances of subtle
pg_class.reltuple estimation variations between VACUUM runs.

Here we adjust the query to something that has a much larger cost
difference between the cheapest and 2nd cheapest plans.  It makes sense to
apply this to all versions and not just v14, as it's generally bad to
always expect the cheapest plan to be the same when there is more than 1
plan that costs equally as cheap.

Reported-by: Alexander Lakhin <exclusion@gmail.com>
Author: David Rowley <dgrowleyml@gmail.com>
Discussion: https://postgr.es/m/f5d1f4c2-6224-4797-be17-c86e77f96c9c@gmail.com
Backpatch-through: 14
---
 src/test/regress/expected/tidscan.out | 47 +++++++++++++++------------
 src/test/regress/sql/tidscan.sql      | 14 ++++----
 2 files changed, 34 insertions(+), 27 deletions(-)

diff --git a/src/test/regress/expected/tidscan.out b/src/test/regress/expected/tidscan.out
index e823bc91c57..ee6046152c8 100644
--- a/src/test/regress/expected/tidscan.out
+++ b/src/test/regress/expected/tidscan.out
@@ -234,21 +234,25 @@ UPDATE tidscan SET id = -id WHERE CURRENT OF c RETURNING *;
 ERROR:  cursor "c" is not positioned on a row
 ROLLBACK;
 -- bulk joins on CTID
--- (these plans don't use TID scans, but this still seems like an
--- appropriate place for these tests)
+-- These plans don't use TID scans, but this still seems like an
+-- appropriate place for these tests.  The slightly unusual DISTINCT semi-join
+-- exists to avoid join-order instability.  A more traditional query for such
+-- a self-join would produce the same cost for either join order.
 EXPLAIN (COSTS OFF)
-SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
-               QUERY PLAN               
-----------------------------------------
+SELECT count(*) FROM tenk1 WHERE ctid IN (SELECT DISTINCT ctid FROM tenk1);
+                    QUERY PLAN                     
+---------------------------------------------------
  Aggregate
    ->  Hash Join
-         Hash Cond: (t1.ctid = t2.ctid)
-         ->  Seq Scan on tenk1 t1
+         Hash Cond: (tenk1.ctid = tenk1_1.ctid)
+         ->  Seq Scan on tenk1
          ->  Hash
-               ->  Seq Scan on tenk1 t2
-(6 rows)
+               ->  HashAggregate
+                     Group Key: tenk1_1.ctid
+                     ->  Seq Scan on tenk1 tenk1_1
+(8 rows)
 
-SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
+SELECT count(*) FROM tenk1 WHERE ctid IN (SELECT DISTINCT ctid FROM tenk1);
  count 
 -------
  10000
@@ -256,21 +260,22 @@ SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
 
 SET enable_hashjoin TO off;
 EXPLAIN (COSTS OFF)
-SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
-               QUERY PLAN                
------------------------------------------
+SELECT count(*) FROM tenk1 WHERE ctid IN (SELECT DISTINCT ctid FROM tenk1);
+                    QUERY PLAN                     
+---------------------------------------------------
  Aggregate
    ->  Merge Join
-         Merge Cond: (t1.ctid = t2.ctid)
+         Merge Cond: (tenk1_1.ctid = tenk1.ctid)
+         ->  Unique
+               ->  Sort
+                     Sort Key: tenk1_1.ctid
+                     ->  Seq Scan on tenk1 tenk1_1
          ->  Sort
-               Sort Key: t1.ctid
-               ->  Seq Scan on tenk1 t1
-         ->  Sort
-               Sort Key: t2.ctid
-               ->  Seq Scan on tenk1 t2
-(9 rows)
+               Sort Key: tenk1.ctid
+               ->  Seq Scan on tenk1
+(10 rows)
 
-SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
+SELECT count(*) FROM tenk1 WHERE ctid IN (SELECT DISTINCT ctid FROM tenk1);
  count 
 -------
  10000
diff --git a/src/test/regress/sql/tidscan.sql b/src/test/regress/sql/tidscan.sql
index 1b82d5f1a53..8f0c946c676 100644
--- a/src/test/regress/sql/tidscan.sql
+++ b/src/test/regress/sql/tidscan.sql
@@ -83,15 +83,17 @@ UPDATE tidscan SET id = -id WHERE CURRENT OF c RETURNING *;
 ROLLBACK;
 
 -- bulk joins on CTID
--- (these plans don't use TID scans, but this still seems like an
--- appropriate place for these tests)
+-- These plans don't use TID scans, but this still seems like an
+-- appropriate place for these tests.  The slightly unusual DISTINCT semi-join
+-- exists to avoid join-order instability.  A more traditional query for such
+-- a self-join would produce the same cost for either join order.
 EXPLAIN (COSTS OFF)
-SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
-SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
+SELECT count(*) FROM tenk1 WHERE ctid IN (SELECT DISTINCT ctid FROM tenk1);
+SELECT count(*) FROM tenk1 WHERE ctid IN (SELECT DISTINCT ctid FROM tenk1);
 SET enable_hashjoin TO off;
 EXPLAIN (COSTS OFF)
-SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
-SELECT count(*) FROM tenk1 t1 JOIN tenk1 t2 ON t1.ctid = t2.ctid;
+SELECT count(*) FROM tenk1 WHERE ctid IN (SELECT DISTINCT ctid FROM tenk1);
+SELECT count(*) FROM tenk1 WHERE ctid IN (SELECT DISTINCT ctid FROM tenk1);
 RESET enable_hashjoin;
 
 -- check predicate lock on CTID
-- 
2.53.0

