From 4aa185a8a85a2d491d02398aaf948706591b431d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Sun, 19 Jul 2026 19:08:17 +0200
Subject: [PATCH v10 03/21] Skip paths with filters in
 get_cheapest_path_for_pathkeys

Make sure get_cheapest_path_for_pathkeys returns a path without any
filters (and same for get_cheapest_fractional_path_for_pathkeys).

This is what merge joins use to get sorted paths, and allowing paths
with filters might easily confuse that. The merge join only looks at the
pair of cheapest paths, but what if the expected filters contradict the
join? That is, the merge join can't satisfy them. The code does not look
at other "cheap" sorted paths (even though they can be just as cheap).

We'd probably still produce a valid join, because we also consider the
cheapest total path with an explicit sort on top, and that ignores
expected filters (i.e. cheapest_total_path never has any filters). But
it can be much more expensive.

Theis is a bit annoying, because it means merge joins kinda "strip" the
filters. It'd be good to make this compatible with filters, at least
in some cases.

XXX We need to be careful to not increase the number of join paths too
much.  For example, with 10 paths on each join side, we probably don't
want to generate a join for all 100 combinations. Maybe we could look at
combinations with "compatible" filters, or something like that?
---
 contrib/pg_plan_advice/expected/gather.out    |  18 ++-
 .../pg_plan_advice/expected/join_strategy.out |  26 ++--
 .../postgres_fdw/expected/postgres_fdw.out    | 136 +++++++++---------
 src/backend/optimizer/path/pathkeys.c         |  30 ++++
 src/test/regress/expected/join.out            |  56 ++++----
 5 files changed, 140 insertions(+), 126 deletions(-)

diff --git a/contrib/pg_plan_advice/expected/gather.out b/contrib/pg_plan_advice/expected/gather.out
index ea6ebefef74..0cc0dedf859 100644
--- a/contrib/pg_plan_advice/expected/gather.out
+++ b/contrib/pg_plan_advice/expected/gather.out
@@ -153,18 +153,17 @@ EXPLAIN (COSTS OFF, PLAN_ADVICE)
          ->  Sort
                Sort Key: f.dim_id
                ->  Parallel Seq Scan on gt_fact f
-   ->  Sort
-         Sort Key: d.id
-         ->  Seq Scan on gt_dim d
+   ->  Index Scan using gt_dim_pkey on gt_dim d
  Supplied Plan Advice:
    GATHER((d d/d.d)) /* partially matched */
  Generated Plan Advice:
    JOIN_ORDER(f d)
    MERGE_JOIN_PLAIN(d)
-   SEQ_SCAN(f d)
+   SEQ_SCAN(f)
+   INDEX_SCAN(d public.gt_dim_pkey)
    GATHER_MERGE(f)
    NO_GATHER(d)
-(18 rows)
+(17 rows)
 
 COMMIT;
 -- Force a Gather or Gather Merge on one relation but no parallelism on other.
@@ -181,19 +180,18 @@ EXPLAIN (COSTS OFF, PLAN_ADVICE)
          ->  Sort
                Sort Key: f.dim_id
                ->  Parallel Seq Scan on gt_fact f
-   ->  Sort
-         Sort Key: d.id
-         ->  Seq Scan on gt_dim d
+   ->  Index Scan using gt_dim_pkey on gt_dim d
  Supplied Plan Advice:
    GATHER_MERGE(f) /* matched */
    NO_GATHER(d) /* matched */
  Generated Plan Advice:
    JOIN_ORDER(f d)
    MERGE_JOIN_PLAIN(d)
-   SEQ_SCAN(f d)
+   SEQ_SCAN(f)
+   INDEX_SCAN(d public.gt_dim_pkey)
    GATHER_MERGE(f)
    NO_GATHER(d)
-(19 rows)
+(18 rows)
 
 SET LOCAL pg_plan_advice.advice = 'gather_merge(d) no_gather(f)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
diff --git a/contrib/pg_plan_advice/expected/join_strategy.out b/contrib/pg_plan_advice/expected/join_strategy.out
index fad68bfd18b..0cb183fec80 100644
--- a/contrib/pg_plan_advice/expected/join_strategy.out
+++ b/contrib/pg_plan_advice/expected/join_strategy.out
@@ -56,45 +56,39 @@ EXPLAIN (COSTS OFF, PLAN_ADVICE)
 SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_MATERIALIZE(d)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
 	SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id;
-                       QUERY PLAN                       
---------------------------------------------------------
+                           QUERY PLAN                           
+----------------------------------------------------------------
  Merge Join
    Disabled: true
    Merge Cond: (f.dim_id = d.id)
    ->  Index Scan using join_fact_dim_id on join_fact f
-   ->  Sort
-         Sort Key: d.id
-         ->  Seq Scan on join_dim d
+   ->  Index Scan using join_dim_pkey on join_dim d
  Supplied Plan Advice:
    MERGE_JOIN_MATERIALIZE(d) /* matched, failed */
  Generated Plan Advice:
    JOIN_ORDER(f d)
    MERGE_JOIN_PLAIN(d)
-   SEQ_SCAN(d)
-   INDEX_SCAN(f public.join_fact_dim_id)
+   INDEX_SCAN(f public.join_fact_dim_id d public.join_dim_pkey)
    NO_GATHER(f d)
-(15 rows)
+(12 rows)
 
 SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_PLAIN(d)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
 	SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id;
-                       QUERY PLAN                       
---------------------------------------------------------
+                           QUERY PLAN                           
+----------------------------------------------------------------
  Merge Join
    Merge Cond: (f.dim_id = d.id)
    ->  Index Scan using join_fact_dim_id on join_fact f
-   ->  Sort
-         Sort Key: d.id
-         ->  Seq Scan on join_dim d
+   ->  Index Scan using join_dim_pkey on join_dim d
  Supplied Plan Advice:
    MERGE_JOIN_PLAIN(d) /* matched */
  Generated Plan Advice:
    JOIN_ORDER(f d)
    MERGE_JOIN_PLAIN(d)
-   SEQ_SCAN(d)
-   INDEX_SCAN(f public.join_fact_dim_id)
+   INDEX_SCAN(f public.join_fact_dim_id d public.join_dim_pkey)
    NO_GATHER(f d)
-(14 rows)
+(11 rows)
 
 SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MATERIALIZE(d)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 27d3387e4f7..4760d177ef8 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -477,21 +477,20 @@ SET enable_nestloop TO false;
 -- inner join; expressions in the clauses appear in the equivalence class list
 EXPLAIN (VERBOSE, COSTS OFF)
 	SELECT t1.c1, t2."C 1" FROM ft2 t1 JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1") OFFSET 100 LIMIT 10;
-                                         QUERY PLAN                                          
----------------------------------------------------------------------------------------------
+                                      QUERY PLAN                                       
+---------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2."C 1"
    ->  Merge Join
          Output: t1.c1, t2."C 1"
-         Merge Cond: (t2."C 1" = t1.c1)
+         Inner Unique: true
+         Merge Cond: (t1.c1 = t2."C 1")
+         ->  Foreign Scan on public.ft2 t1
+               Output: t1.c1
+               Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
          ->  Index Only Scan using t1_pkey on "S 1"."T 1" t2
                Output: t2."C 1"
-         ->  Materialize
-               Output: t1.c1
-               ->  Foreign Scan on public.ft2 t1
-                     Output: t1.c1
-                     Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
-(12 rows)
+(11 rows)
 
 SELECT t1.c1, t2."C 1" FROM ft2 t1 JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1") OFFSET 100 LIMIT 10;
  c1  | C 1 
@@ -546,22 +545,21 @@ SELECT t1.c1, t2."C 1" FROM ft2 t1 LEFT JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1"
 -- foreign join so that the local table can be joined using merge join strategy.
 EXPLAIN (VERBOSE, COSTS OFF)
 	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
-                                                                          QUERY PLAN                                                                           
----------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                       QUERY PLAN                                                                        
+---------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1."C 1"
-   ->  Merge Left Join
+   ->  Merge Right Join
          Output: t1."C 1"
-         Merge Cond: (t1."C 1" = t3.c1)
+         Inner Unique: true
+         Merge Cond: (t3.c1 = t1."C 1")
+         ->  Foreign Scan
+               Output: t3.c1
+               Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
+               Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
          ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                Output: t1."C 1"
-         ->  Materialize
-               Output: t3.c1
-               ->  Foreign Scan
-                     Output: t3.c1
-                     Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
-                     Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
-(13 rows)
+(12 rows)
 
 SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
  C 1 
@@ -583,22 +581,21 @@ SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.
 -- included in join restrictions.
 EXPLAIN (VERBOSE, COSTS OFF)
 	SELECT t1."C 1", t2.c1, t3.c1 FROM "S 1"."T 1" t1 left join ft1 t2 full join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
-                                                                               QUERY PLAN                                                                               
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                            QUERY PLAN                                                                            
+------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1."C 1", t2.c1, t3.c1
-   ->  Merge Left Join
+   ->  Merge Right Join
          Output: t1."C 1", t2.c1, t3.c1
-         Merge Cond: (t1."C 1" = t3.c1)
+         Inner Unique: true
+         Merge Cond: (t3.c1 = t1."C 1")
+         ->  Foreign Scan
+               Output: t3.c1, t2.c1
+               Relations: (public.ft2 t3) LEFT JOIN (public.ft1 t2)
+               Remote SQL: SELECT r3."C 1", r2."C 1" FROM ("S 1"."T 1" r3 LEFT JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r3."C 1")))) ORDER BY r3."C 1" ASC NULLS LAST
          ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                Output: t1."C 1"
-         ->  Materialize
-               Output: t3.c1, t2.c1
-               ->  Foreign Scan
-                     Output: t3.c1, t2.c1
-                     Relations: (public.ft2 t3) LEFT JOIN (public.ft1 t2)
-                     Remote SQL: SELECT r3."C 1", r2."C 1" FROM ("S 1"."T 1" r3 LEFT JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r3."C 1")))) ORDER BY r3."C 1" ASC NULLS LAST
-(13 rows)
+(12 rows)
 
 SELECT t1."C 1", t2.c1, t3.c1 FROM "S 1"."T 1" t1 left join ft1 t2 full join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
  C 1 | c1  | c1  
@@ -2615,57 +2612,56 @@ SET enable_hashjoin TO false;
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT * FROM ft1, ft2, ft4, ft5, local_tbl WHERE ft1.c1 = ft2.c1 AND ft1.c2 = ft4.c1
     AND ft1.c2 = ft5.c1 AND ft1.c2 = local_tbl.c1 AND ft1.c1 < 100 AND ft2.c1 < 100 FOR UPDATE;
-                                                                                                                                                                                                                                                                                                                                                                                                                                                  QUERY PLAN                                                                                                                                                                                                                                                                                                                                                                                                                                                  
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                                                                                                                                                                                                                                                                                               QUERY PLAN                                                                                                                                                                                                                                                                                                                                                                                                                                               
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  LockRows
    Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3, local_tbl.c1, local_tbl.c2, local_tbl.c3, ft1.*, ft2.*, ft4.*, ft5.*, local_tbl.ctid
    ->  Merge Join
          Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3, local_tbl.c1, local_tbl.c2, local_tbl.c3, ft1.*, ft2.*, ft4.*, ft5.*, local_tbl.ctid
-         Merge Cond: (local_tbl.c1 = ft1.c2)
-         ->  Index Scan using local_tbl_pkey on public.local_tbl
-               Output: local_tbl.c1, local_tbl.c2, local_tbl.c3, local_tbl.ctid
-         ->  Materialize
+         Inner Unique: true
+         Merge Cond: (ft1.c2 = local_tbl.c1)
+         ->  Foreign Scan
                Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*, ft4.c1, ft4.c2, ft4.c3, ft4.*, ft5.c1, ft5.c2, ft5.c3, ft5.*
-               ->  Foreign Scan
+               Relations: (((public.ft1) INNER JOIN (public.ft2)) INNER JOIN (public.ft4)) INNER JOIN (public.ft5)
+               Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END, r3.c1, r3.c2, r3.c3, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r4.c1, r4.c2, r4.c3, CASE WHEN (r4.*)::text IS NOT NULL THEN ROW(r4.c1, r4.c2, r4.c3) END FROM ((("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r2."C 1" < 100)) AND ((r1."C 1" < 100)))) INNER JOIN "S 1"."T 3" r3 ON (((r1.c2 = r3.c1)))) INNER JOIN "S 1"."T 4" r4 ON (((r1.c2 = r4.c1)))) ORDER BY r1.c2 ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2 FOR UPDATE OF r3 FOR UPDATE OF r4
+               ->  Merge Join
                      Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*, ft4.c1, ft4.c2, ft4.c3, ft4.*, ft5.c1, ft5.c2, ft5.c3, ft5.*
-                     Relations: (((public.ft1) INNER JOIN (public.ft2)) INNER JOIN (public.ft4)) INNER JOIN (public.ft5)
-                     Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END, r3.c1, r3.c2, r3.c3, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r4.c1, r4.c2, r4.c3, CASE WHEN (r4.*)::text IS NOT NULL THEN ROW(r4.c1, r4.c2, r4.c3) END FROM ((("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r2."C 1" < 100)) AND ((r1."C 1" < 100)))) INNER JOIN "S 1"."T 3" r3 ON (((r1.c2 = r3.c1)))) INNER JOIN "S 1"."T 4" r4 ON (((r1.c2 = r4.c1)))) ORDER BY r1.c2 ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2 FOR UPDATE OF r3 FOR UPDATE OF r4
+                     Merge Cond: (ft1.c2 = ft5.c1)
                      ->  Merge Join
-                           Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*, ft4.c1, ft4.c2, ft4.c3, ft4.*, ft5.c1, ft5.c2, ft5.c3, ft5.*
-                           Merge Cond: (ft1.c2 = ft5.c1)
-                           ->  Merge Join
-                                 Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*, ft4.c1, ft4.c2, ft4.c3, ft4.*
-                                 Merge Cond: (ft1.c2 = ft4.c1)
-                                 ->  Sort
+                           Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*, ft4.c1, ft4.c2, ft4.c3, ft4.*
+                           Merge Cond: (ft1.c2 = ft4.c1)
+                           ->  Sort
+                                 Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*
+                                 Sort Key: ft1.c2
+                                 ->  Merge Join
                                        Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*
-                                       Sort Key: ft1.c2
-                                       ->  Merge Join
-                                             Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*
-                                             Merge Cond: (ft1.c1 = ft2.c1)
-                                             ->  Sort
+                                       Merge Cond: (ft1.c1 = ft2.c1)
+                                       ->  Sort
+                                             Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*
+                                             Sort Key: ft1.c1
+                                             ->  Foreign Scan on public.ft1
                                                    Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*
-                                                   Sort Key: ft1.c1
-                                                   ->  Foreign Scan on public.ft1
-                                                         Output: ft1.c1, ft1.c2, ft1.c3, ft1.c4, ft1.c5, ft1.c6, ft1.c7, ft1.c8, ft1.*
-                                                         Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 100)) FOR UPDATE
-                                             ->  Materialize
+                                                   Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 100)) FOR UPDATE
+                                       ->  Materialize
+                                             Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*
+                                             ->  Foreign Scan on public.ft2
                                                    Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*
-                                                   ->  Foreign Scan on public.ft2
-                                                         Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.*
-                                                         Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 100)) ORDER BY "C 1" ASC NULLS LAST FOR UPDATE
-                                 ->  Sort
-                                       Output: ft4.c1, ft4.c2, ft4.c3, ft4.*
-                                       Sort Key: ft4.c1
-                                       ->  Foreign Scan on public.ft4
-                                             Output: ft4.c1, ft4.c2, ft4.c3, ft4.*
-                                             Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 3" FOR UPDATE
+                                                   Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 100)) ORDER BY "C 1" ASC NULLS LAST FOR UPDATE
                            ->  Sort
+                                 Output: ft4.c1, ft4.c2, ft4.c3, ft4.*
+                                 Sort Key: ft4.c1
+                                 ->  Foreign Scan on public.ft4
+                                       Output: ft4.c1, ft4.c2, ft4.c3, ft4.*
+                                       Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 3" FOR UPDATE
+                     ->  Sort
+                           Output: ft5.c1, ft5.c2, ft5.c3, ft5.*
+                           Sort Key: ft5.c1
+                           ->  Foreign Scan on public.ft5
                                  Output: ft5.c1, ft5.c2, ft5.c3, ft5.*
-                                 Sort Key: ft5.c1
-                                 ->  Foreign Scan on public.ft5
-                                       Output: ft5.c1, ft5.c2, ft5.c3, ft5.*
-                                       Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 4" FOR UPDATE
-(48 rows)
+                                 Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 4" FOR UPDATE
+         ->  Index Scan using local_tbl_pkey on public.local_tbl
+               Output: local_tbl.c1, local_tbl.c2, local_tbl.c3, local_tbl.ctid
+(47 rows)
 
 SELECT * FROM ft1, ft2, ft4, ft5, local_tbl WHERE ft1.c1 = ft2.c1 AND ft1.c2 = ft4.c1
     AND ft1.c2 = ft5.c1 AND ft1.c2 = local_tbl.c1 AND ft1.c1 < 100 AND ft2.c1 < 100 FOR UPDATE;
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index d6a305aedac..f4542b2bf43 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -633,6 +633,21 @@ get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
 		if (require_parallel_safe && !path->parallel_safe)
 			continue;
 
+		/*
+		 * XXX We should really make this useful with pushed-down filters,
+		 * when possible. But we have to disable that for now (by default),
+		 * because otherwise it'd confuse merge joins - those simply get the
+		 * cheapest sorted paths, and that's it. So we'd need to make sure to
+		 * only consider paths that don't have conflicting filters (which the
+		 * merge join can't satisfy).
+		 *
+		 * Furthermore, it'd mean the join has to consider combinations of
+		 * inner/outer paths, while now it simply picks the cheapest ones and
+		 * that's it.
+		 */
+		if (path->expected_filters != NIL)
+			continue;
+
 		/*
 		 * Since cost comparison is a lot cheaper than pathkey comparison, do
 		 * that first.  (XXX is that still true?)
@@ -675,6 +690,21 @@ get_cheapest_fractional_path_for_pathkeys(List *paths,
 	{
 		Path	   *path = (Path *) lfirst(l);
 
+		/*
+		 * XXX We should really make this useful with pushed-down filters,
+		 * when possible. But we have to disable that for now (by default),
+		 * because otherwise it'd confuse merge joins - those simply get the
+		 * cheapest sorted paths, and that's it. So we'd need to make sure to
+		 * only consider paths that don't have conflicting filters (which the
+		 * merge join can't satisfy).
+		 *
+		 * Furthermore, it'd mean the join has to consider combinations of
+		 * inner/outer paths, while now it simply picks the cheapest ones and
+		 * that's it.
+		 */
+		if (path->expected_filters != NIL)
+			continue;
+
 		/*
 		 * Since cost comparison is a lot cheaper than pathkey comparison, do
 		 * that first.  (XXX is that still true?)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d78ce4fdf97..447ad4fe702 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -10623,16 +10623,14 @@ create index j2_id1_idx on j2 (id1) where id1 % 1000 = 1;
 explain (costs off) select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
 where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
-                        QUERY PLAN                        
-----------------------------------------------------------
- Nested Loop
-   Disabled: true
-   Join Filter: ((j2.id1 = j1.id1) AND (j2.id2 = j1.id2))
-   ->  Seq Scan on j1
-         Filter: ((id1 % 1000) = 1)
-   ->  Seq Scan on j2
-         Filter: ((id1 % 1000) = 1)
-(7 rows)
+               QUERY PLAN                
+-----------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j2.id2 = j1.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j2_id1_idx on j2
+(5 rows)
 
 select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
@@ -10647,16 +10645,15 @@ where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1;
 explain (costs off) select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
 where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 = any (array[1]);
-                               QUERY PLAN                                
--------------------------------------------------------------------------
- Nested Loop
-   Disabled: true
-   Join Filter: ((j2.id1 = j1.id1) AND (j2.id2 = j1.id2))
-   ->  Seq Scan on j1
-         Filter: ((id1 % 1000) = 1)
-   ->  Seq Scan on j2
-         Filter: ((id1 = ANY ('{1}'::integer[])) AND ((id1 % 1000) = 1))
-(7 rows)
+                     QUERY PLAN                     
+----------------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j2.id2 = j1.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j2_id1_idx on j2
+         Index Cond: (id1 = ANY ('{1}'::integer[]))
+(6 rows)
 
 select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
@@ -10671,16 +10668,15 @@ where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 = any (array[1]);
 explain (costs off) select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
 where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 >= any (array[1,5]);
-                                 QUERY PLAN                                 
-----------------------------------------------------------------------------
- Nested Loop
-   Disabled: true
-   Join Filter: ((j2.id1 = j1.id1) AND (j2.id2 = j1.id2))
-   ->  Seq Scan on j1
-         Filter: ((id1 % 1000) = 1)
-   ->  Seq Scan on j2
-         Filter: ((id1 >= ANY ('{1,5}'::integer[])) AND ((id1 % 1000) = 1))
-(7 rows)
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Merge Join
+   Merge Cond: (j1.id1 = j2.id1)
+   Join Filter: (j2.id2 = j1.id2)
+   ->  Index Scan using j1_id1_idx on j1
+   ->  Index Scan using j2_id1_idx on j2
+         Index Cond: (id1 >= ANY ('{1,5}'::integer[]))
+(6 rows)
 
 select * from j1
 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2
-- 
2.50.1 (Apple Git-155)

