diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index cff23b0211..9f29d7f2b8 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -1275,7 +1275,7 @@ deparseLockingClause(deparse_expr_cxt *context)
 		 * that DECLARE CURSOR ... FOR UPDATE is supported, which it isn't
 		 * before 8.3.
 		 */
-		if (relid == root->parse->resultRelation &&
+		if (bms_is_member(relid, root->all_result_relids) &&
 			(root->parse->commandType == CMD_UPDATE ||
 			 root->parse->commandType == CMD_DELETE))
 		{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index b46e7e623f..8d8a7b88df 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -6348,13 +6348,13 @@ SELECT * FROM foreign_tbl;
 
 EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE rw_view SET b = b + 5;
-                                       QUERY PLAN                                       
-----------------------------------------------------------------------------------------
+                                           QUERY PLAN                                           
+------------------------------------------------------------------------------------------------
  Update on public.parent_tbl
    Foreign Update on public.foreign_tbl parent_tbl_1
      Remote SQL: UPDATE public.child_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
    ->  Foreign Scan on public.foreign_tbl parent_tbl_1
-         Output: (parent_tbl_1.b + 5), parent_tbl_1.ctid, parent_tbl_1.*
+         Output: (parent_tbl_1.b + 5), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.*
          Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE
 (6 rows)
 
@@ -6363,13 +6363,13 @@ ERROR:  new row violates check option for view "rw_view"
 DETAIL:  Failing row contains (20, 20).
 EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE rw_view SET b = b + 15;
-                                       QUERY PLAN                                       
-----------------------------------------------------------------------------------------
+                                           QUERY PLAN                                            
+-------------------------------------------------------------------------------------------------
  Update on public.parent_tbl
    Foreign Update on public.foreign_tbl parent_tbl_1
      Remote SQL: UPDATE public.child_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
    ->  Foreign Scan on public.foreign_tbl parent_tbl_1
-         Output: (parent_tbl_1.b + 15), parent_tbl_1.ctid, parent_tbl_1.*
+         Output: (parent_tbl_1.b + 15), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.*
          Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE
 (6 rows)
 
@@ -7253,36 +7253,22 @@ select * from bar where f1 in (select f1 from foo) for share;
 -- Check UPDATE with inherited target and an inherited source table
 explain (verbose, costs off)
 update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
-                                      QUERY PLAN                                       
----------------------------------------------------------------------------------------
+                                              QUERY PLAN                                               
+-------------------------------------------------------------------------------------------------------
  Update on public.bar
-   Update on public.bar
-   Foreign Update on public.bar2 bar_1
+   Update on public.bar bar_1
+   Foreign Update on public.bar2 bar_2
      Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1
    ->  Hash Join
-         Output: (bar.f2 + 100), bar.ctid, foo.ctid, foo.*, foo.tableoid
+         Output: (bar.f2 + 100), foo.ctid, bar.tableoid, bar.ctid, (NULL::record), foo.*, foo.tableoid
          Inner Unique: true
          Hash Cond: (bar.f1 = foo.f1)
-         ->  Seq Scan on public.bar
-               Output: bar.f2, bar.ctid, bar.f1
-         ->  Hash
-               Output: foo.ctid, foo.f1, foo.*, foo.tableoid
-               ->  HashAggregate
-                     Output: foo.ctid, foo.f1, foo.*, foo.tableoid
-                     Group Key: foo.f1
-                     ->  Append
-                           ->  Seq Scan on public.foo foo_1
-                                 Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
-                           ->  Foreign Scan on public.foo2 foo_2
-                                 Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
-                                 Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-   ->  Hash Join
-         Output: (bar_1.f2 + 100), bar_1.ctid, bar_1.*, foo.ctid, foo.*, foo.tableoid
-         Inner Unique: true
-         Hash Cond: (bar_1.f1 = foo.f1)
-         ->  Foreign Scan on public.bar2 bar_1
-               Output: bar_1.f2, bar_1.ctid, bar_1.*, bar_1.f1
-               Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
+         ->  Append
+               ->  Seq Scan on public.bar bar_1
+                     Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::record
+               ->  Foreign Scan on public.bar2 bar_2
+                     Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, bar_2.*
+                     Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
          ->  Hash
                Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                ->  HashAggregate
@@ -7294,7 +7280,7 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(39 rows)
+(25 rows)
 
 update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
 select tableoid::regclass, * from bar order by 1,2;
@@ -7314,39 +7300,24 @@ update bar set f2 = f2 + 100
 from
   ( select f1 from foo union all select f1+3 from foo ) ss
 where bar.f1 = ss.f1;
-                                      QUERY PLAN                                      
---------------------------------------------------------------------------------------
+                                           QUERY PLAN                                           
+------------------------------------------------------------------------------------------------
  Update on public.bar
-   Update on public.bar
-   Foreign Update on public.bar2 bar_1
+   Update on public.bar bar_1
+   Foreign Update on public.bar2 bar_2
      Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1
-   ->  Hash Join
-         Output: (bar.f2 + 100), bar.ctid, (ROW(foo.f1))
-         Hash Cond: (foo.f1 = bar.f1)
-         ->  Append
-               ->  Seq Scan on public.foo
-                     Output: ROW(foo.f1), foo.f1
-               ->  Foreign Scan on public.foo2 foo_1
-                     Output: ROW(foo_1.f1), foo_1.f1
-                     Remote SQL: SELECT f1 FROM public.loct1
-               ->  Seq Scan on public.foo foo_2
-                     Output: ROW((foo_2.f1 + 3)), (foo_2.f1 + 3)
-               ->  Foreign Scan on public.foo2 foo_3
-                     Output: ROW((foo_3.f1 + 3)), (foo_3.f1 + 3)
-                     Remote SQL: SELECT f1 FROM public.loct1
-         ->  Hash
-               Output: bar.f2, bar.ctid, bar.f1
-               ->  Seq Scan on public.bar
-                     Output: bar.f2, bar.ctid, bar.f1
    ->  Merge Join
-         Output: (bar_1.f2 + 100), bar_1.ctid, bar_1.*, (ROW(foo.f1))
-         Merge Cond: (bar_1.f1 = foo.f1)
+         Output: (bar.f2 + 100), (ROW(foo.f1)), bar.tableoid, bar.ctid, (NULL::record)
+         Merge Cond: (bar.f1 = foo.f1)
          ->  Sort
-               Output: bar_1.f2, bar_1.ctid, bar_1.*, bar_1.f1
-               Sort Key: bar_1.f1
-               ->  Foreign Scan on public.bar2 bar_1
-                     Output: bar_1.f2, bar_1.ctid, bar_1.*, bar_1.f1
-                     Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
+               Output: bar.f2, bar.f1, bar.tableoid, bar.ctid, (NULL::record)
+               Sort Key: bar.f1
+               ->  Append
+                     ->  Seq Scan on public.bar bar_1
+                           Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::record
+                     ->  Foreign Scan on public.bar2 bar_2
+                           Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, bar_2.*
+                           Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
          ->  Sort
                Output: (ROW(foo.f1)), foo.f1
                Sort Key: foo.f1
@@ -7361,7 +7332,7 @@ where bar.f1 = ss.f1;
                      ->  Foreign Scan on public.foo2 foo_3
                            Output: ROW((foo_3.f1 + 3)), (foo_3.f1 + 3)
                            Remote SQL: SELECT f1 FROM public.loct1
-(45 rows)
+(30 rows)
 
 update bar set f2 = f2 + 100
 from
@@ -7487,18 +7458,19 @@ ERROR:  WHERE CURRENT OF is not supported for this table type
 rollback;
 explain (verbose, costs off)
 delete from foo where f1 < 5 returning *;
-                                   QUERY PLAN                                   
---------------------------------------------------------------------------------
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
  Delete on public.foo
-   Output: foo.f1, foo.f2
-   Delete on public.foo
-   Foreign Delete on public.foo2 foo_1
-   ->  Index Scan using i_foo_f1 on public.foo
-         Output: foo.ctid
-         Index Cond: (foo.f1 < 5)
-   ->  Foreign Delete on public.foo2 foo_1
-         Remote SQL: DELETE FROM public.loct1 WHERE ((f1 < 5)) RETURNING f1, f2
-(9 rows)
+   Output: foo_1.f1, foo_1.f2
+   Delete on public.foo foo_1
+   Foreign Delete on public.foo2 foo_2
+   ->  Append
+         ->  Index Scan using i_foo_f1 on public.foo foo_1
+               Output: foo_1.tableoid, foo_1.ctid
+               Index Cond: (foo_1.f1 < 5)
+         ->  Foreign Delete on public.foo2 foo_2
+               Remote SQL: DELETE FROM public.loct1 WHERE ((f1 < 5)) RETURNING f1, f2
+(10 rows)
 
 delete from foo where f1 < 5 returning *;
  f1 | f2 
@@ -7512,17 +7484,20 @@ delete from foo where f1 < 5 returning *;
 
 explain (verbose, costs off)
 update bar set f2 = f2 + 100 returning *;
-                                  QUERY PLAN                                  
-------------------------------------------------------------------------------
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
  Update on public.bar
-   Output: bar.f1, bar.f2
-   Update on public.bar
-   Foreign Update on public.bar2 bar_1
-   ->  Seq Scan on public.bar
-         Output: (bar.f2 + 100), bar.ctid
-   ->  Foreign Update on public.bar2 bar_1
-         Remote SQL: UPDATE public.loct2 SET f2 = (f2 + 100) RETURNING f1, f2
-(8 rows)
+   Output: bar_1.f1, bar_1.f2
+   Update on public.bar bar_1
+   Foreign Update on public.bar2 bar_2
+   ->  Result
+         Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::record)
+         ->  Append
+               ->  Seq Scan on public.bar bar_1
+                     Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::record
+               ->  Foreign Update on public.bar2 bar_2
+                     Remote SQL: UPDATE public.loct2 SET f2 = (f2 + 100) RETURNING f1, f2
+(11 rows)
 
 update bar set f2 = f2 + 100 returning *;
  f1 | f2  
@@ -7547,15 +7522,18 @@ update bar set f2 = f2 + 100;
                                                QUERY PLAN                                               
 --------------------------------------------------------------------------------------------------------
  Update on public.bar
-   Update on public.bar
-   Foreign Update on public.bar2 bar_1
+   Update on public.bar bar_1
+   Foreign Update on public.bar2 bar_2
      Remote SQL: UPDATE public.loct2 SET f1 = $2, f2 = $3, f3 = $4 WHERE ctid = $1 RETURNING f1, f2, f3
-   ->  Seq Scan on public.bar
-         Output: (bar.f2 + 100), bar.ctid
-   ->  Foreign Scan on public.bar2 bar_1
-         Output: (bar_1.f2 + 100), bar_1.ctid, bar_1.*
-         Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
-(9 rows)
+   ->  Result
+         Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::record)
+         ->  Append
+               ->  Seq Scan on public.bar bar_1
+                     Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::record
+               ->  Foreign Scan on public.bar2 bar_2
+                     Output: bar_2.f2, bar_2.tableoid, bar_2.ctid, bar_2.*
+                     Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
+(12 rows)
 
 update bar set f2 = f2 + 100;
 NOTICE:  trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2
@@ -7572,19 +7550,20 @@ NOTICE:  trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2
 NOTICE:  OLD: (7,277,77),NEW: (7,377,77)
 explain (verbose, costs off)
 delete from bar where f2 < 400;
-                                         QUERY PLAN                                          
----------------------------------------------------------------------------------------------
+                                            QUERY PLAN                                             
+---------------------------------------------------------------------------------------------------
  Delete on public.bar
-   Delete on public.bar
-   Foreign Delete on public.bar2 bar_1
+   Delete on public.bar bar_1
+   Foreign Delete on public.bar2 bar_2
      Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 RETURNING f1, f2, f3
-   ->  Seq Scan on public.bar
-         Output: bar.ctid
-         Filter: (bar.f2 < 400)
-   ->  Foreign Scan on public.bar2 bar_1
-         Output: bar_1.ctid, bar_1.*
-         Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE
-(10 rows)
+   ->  Append
+         ->  Seq Scan on public.bar bar_1
+               Output: bar_1.tableoid, bar_1.ctid, NULL::record
+               Filter: (bar_1.f2 < 400)
+         ->  Foreign Scan on public.bar2 bar_2
+               Output: bar_2.tableoid, bar_2.ctid, bar_2.*
+               Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE
+(11 rows)
 
 delete from bar where f2 < 400;
 NOTICE:  trig_row_before(23, skidoo) BEFORE ROW DELETE ON bar2
@@ -7615,23 +7594,28 @@ analyze remt1;
 analyze remt2;
 explain (verbose, costs off)
 update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *;
-                                                                  QUERY PLAN                                                                   
------------------------------------------------------------------------------------------------------------------------------------------------
+                                                   QUERY PLAN                                                   
+----------------------------------------------------------------------------------------------------------------
  Update on public.parent
-   Output: parent.a, parent.b, remt2.a, remt2.b
-   Update on public.parent
-   Foreign Update on public.remt1 parent_1
+   Output: parent_1.a, parent_1.b, remt2.a, remt2.b
+   Update on public.parent parent_1
+   Foreign Update on public.remt1 parent_2
+     Remote SQL: UPDATE public.loct1 SET b = $2 WHERE ctid = $1 RETURNING a, b
    ->  Nested Loop
-         Output: (parent.b || remt2.b), parent.ctid, remt2.*, remt2.a, remt2.b
+         Output: (parent.b || remt2.b), remt2.*, remt2.a, remt2.b, parent.tableoid, parent.ctid, (NULL::record)
          Join Filter: (parent.a = remt2.a)
-         ->  Seq Scan on public.parent
-               Output: parent.b, parent.ctid, parent.a
-         ->  Foreign Scan on public.remt2
+         ->  Append
+               ->  Seq Scan on public.parent parent_1
+                     Output: parent_1.b, parent_1.a, parent_1.tableoid, parent_1.ctid, NULL::record
+               ->  Foreign Scan on public.remt1 parent_2
+                     Output: parent_2.b, parent_2.a, parent_2.tableoid, parent_2.ctid, parent_2.*
+                     Remote SQL: SELECT a, b, ctid FROM public.loct1 FOR UPDATE
+         ->  Materialize
                Output: remt2.b, remt2.*, remt2.a
-               Remote SQL: SELECT a, b FROM public.loct2
-   ->  Foreign Update
-         Remote SQL: UPDATE public.loct1 r4 SET b = (r4.b || r2.b) FROM public.loct2 r2 WHERE ((r4.a = r2.a)) RETURNING r4.a, r4.b, r2.a, r2.b
-(14 rows)
+               ->  Foreign Scan on public.remt2
+                     Output: remt2.b, remt2.*, remt2.a
+                     Remote SQL: SELECT a, b FROM public.loct2
+(19 rows)
 
 update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *;
  a |   b    | a |  b  
@@ -7642,23 +7626,28 @@ update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a re
 
 explain (verbose, costs off)
 delete from parent using remt2 where parent.a = remt2.a returning parent;
-                                                    QUERY PLAN                                                    
-------------------------------------------------------------------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Delete on public.parent
-   Output: parent.*
-   Delete on public.parent
-   Foreign Delete on public.remt1 parent_1
+   Output: parent_1.*
+   Delete on public.parent parent_1
+   Foreign Delete on public.remt1 parent_2
+     Remote SQL: DELETE FROM public.loct1 WHERE ctid = $1 RETURNING a, b
    ->  Nested Loop
-         Output: parent.ctid, remt2.*
+         Output: remt2.*, parent.tableoid, parent.ctid
          Join Filter: (parent.a = remt2.a)
-         ->  Seq Scan on public.parent
-               Output: parent.ctid, parent.a
-         ->  Foreign Scan on public.remt2
+         ->  Append
+               ->  Seq Scan on public.parent parent_1
+                     Output: parent_1.a, parent_1.tableoid, parent_1.ctid
+               ->  Foreign Scan on public.remt1 parent_2
+                     Output: parent_2.a, parent_2.tableoid, parent_2.ctid
+                     Remote SQL: SELECT a, ctid FROM public.loct1 FOR UPDATE
+         ->  Materialize
                Output: remt2.*, remt2.a
-               Remote SQL: SELECT a, b FROM public.loct2
-   ->  Foreign Delete
-         Remote SQL: DELETE FROM public.loct1 r4 USING public.loct2 r2 WHERE ((r4.a = r2.a)) RETURNING r4.a, r4.b
-(14 rows)
+               ->  Foreign Scan on public.remt2
+                     Output: remt2.*, remt2.a
+                     Remote SQL: SELECT a, b FROM public.loct2
+(19 rows)
 
 delete from parent using remt2 where parent.a = remt2.a returning parent;
    parent   
@@ -7837,29 +7826,25 @@ DETAIL:  Failing row contains (2, foo).
 CONTEXT:  remote SQL command: UPDATE public.loct SET a = 2 WHERE ((b = 'foo'::text)) RETURNING a, b
 -- But the reverse is allowed
 update utrtest set a = 1 where b = 'qux' returning *;
- a |  b  
----+-----
- 1 | qux
-(1 row)
-
+ERROR:  cannot route tuples into foreign table to be updated "remp"
 select tableoid::regclass, * FROM utrtest;
  tableoid | a |  b  
 ----------+---+-----
  remp     | 1 | foo
- remp     | 1 | qux
+ locp     | 2 | qux
 (2 rows)
 
 select tableoid::regclass, * FROM remp;
  tableoid | a |  b  
 ----------+---+-----
  remp     | 1 | foo
- remp     | 1 | qux
-(2 rows)
+(1 row)
 
 select tableoid::regclass, * FROM locp;
- tableoid | a | b 
-----------+---+---
-(0 rows)
+ tableoid | a |  b  
+----------+---+-----
+ locp     | 2 | qux
+(1 row)
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
@@ -7871,38 +7856,35 @@ insert into utrtest values (2, 'qux');
 -- Check case where the foreign partition is a subplan target rel
 explain (verbose, costs off)
 update utrtest set a = 1 where a = 1 or a = 2 returning *;
-                                          QUERY PLAN                                          
-----------------------------------------------------------------------------------------------
+                                             QUERY PLAN                                             
+----------------------------------------------------------------------------------------------------
  Update on public.utrtest
    Output: utrtest_1.a, utrtest_1.b
    Foreign Update on public.remp utrtest_1
    Update on public.locp utrtest_2
-   ->  Foreign Update on public.remp utrtest_1
-         Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
-   ->  Seq Scan on public.locp utrtest_2
-         Output: 1, utrtest_2.ctid
-         Filter: ((utrtest_2.a = 1) OR (utrtest_2.a = 2))
-(9 rows)
+   ->  Append
+         ->  Foreign Update on public.remp utrtest_1
+               Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
+         ->  Seq Scan on public.locp utrtest_2
+               Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::record
+               Filter: ((utrtest_2.a = 1) OR (utrtest_2.a = 2))
+(10 rows)
 
 -- The new values are concatenated with ' triggered !'
 update utrtest set a = 1 where a = 1 or a = 2 returning *;
- a |        b        
----+-----------------
- 1 | qux triggered !
-(1 row)
-
+ERROR:  cannot route tuples into foreign table to be updated "remp"
 delete from utrtest;
 insert into utrtest values (2, 'qux');
 -- Check case where the foreign partition isn't a subplan target rel
 explain (verbose, costs off)
 update utrtest set a = 1 where a = 2 returning *;
-               QUERY PLAN                
------------------------------------------
+                      QUERY PLAN                       
+-------------------------------------------------------
  Update on public.utrtest
    Output: utrtest_1.a, utrtest_1.b
    Update on public.locp utrtest_1
    ->  Seq Scan on public.locp utrtest_1
-         Output: 1, utrtest_1.ctid
+         Output: 1, utrtest_1.tableoid, utrtest_1.ctid
          Filter: (utrtest_1.a = 2)
 (6 rows)
 
@@ -7923,61 +7905,51 @@ insert into utrtest values (2, 'qux');
 -- with a direct modification plan
 explain (verbose, costs off)
 update utrtest set a = 1 returning *;
-                           QUERY PLAN                            
------------------------------------------------------------------
+                                QUERY PLAN                                 
+---------------------------------------------------------------------------
  Update on public.utrtest
    Output: utrtest_1.a, utrtest_1.b
    Foreign Update on public.remp utrtest_1
    Update on public.locp utrtest_2
-   ->  Foreign Update on public.remp utrtest_1
-         Remote SQL: UPDATE public.loct SET a = 1 RETURNING a, b
-   ->  Seq Scan on public.locp utrtest_2
-         Output: 1, utrtest_2.ctid
-(8 rows)
+   ->  Append
+         ->  Foreign Update on public.remp utrtest_1
+               Remote SQL: UPDATE public.loct SET a = 1 RETURNING a, b
+         ->  Seq Scan on public.locp utrtest_2
+               Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::record
+(9 rows)
 
 update utrtest set a = 1 returning *;
- a |  b  
----+-----
- 1 | foo
- 1 | qux
-(2 rows)
-
+ERROR:  cannot route tuples into foreign table to be updated "remp"
 delete from utrtest;
 insert into utrtest values (1, 'foo');
 insert into utrtest values (2, 'qux');
 -- with a non-direct modification plan
 explain (verbose, costs off)
 update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
-                                    QUERY PLAN                                    
-----------------------------------------------------------------------------------
+                                           QUERY PLAN                                           
+------------------------------------------------------------------------------------------------
  Update on public.utrtest
    Output: utrtest_1.a, utrtest_1.b, "*VALUES*".column1
    Foreign Update on public.remp utrtest_1
      Remote SQL: UPDATE public.loct SET a = $2 WHERE ctid = $1 RETURNING a, b
    Update on public.locp utrtest_2
    ->  Hash Join
-         Output: 1, utrtest_1.ctid, utrtest_1.*, "*VALUES*".*, "*VALUES*".column1
-         Hash Cond: (utrtest_1.a = "*VALUES*".column1)
-         ->  Foreign Scan on public.remp utrtest_1
-               Output: utrtest_1.ctid, utrtest_1.*, utrtest_1.a
-               Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE
-         ->  Hash
-               Output: "*VALUES*".*, "*VALUES*".column1
-               ->  Values Scan on "*VALUES*"
-                     Output: "*VALUES*".*, "*VALUES*".column1
-   ->  Hash Join
-         Output: 1, utrtest_2.ctid, "*VALUES*".*, "*VALUES*".column1
-         Hash Cond: (utrtest_2.a = "*VALUES*".column1)
-         ->  Seq Scan on public.locp utrtest_2
-               Output: utrtest_2.ctid, utrtest_2.a
+         Output: 1, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, utrtest.*
+         Hash Cond: (utrtest.a = "*VALUES*".column1)
+         ->  Append
+               ->  Foreign Scan on public.remp utrtest_1
+                     Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, utrtest_1.*
+                     Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE
+               ->  Seq Scan on public.locp utrtest_2
+                     Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, NULL::record
          ->  Hash
                Output: "*VALUES*".*, "*VALUES*".column1
                ->  Values Scan on "*VALUES*"
                      Output: "*VALUES*".*, "*VALUES*".column1
-(24 rows)
+(18 rows)
 
 update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
-ERROR:  invalid attribute number 5
+ERROR:  cannot route tuples into foreign table to be updated "remp"
 -- Change the definition of utrtest so that the foreign partition get updated
 -- after the local partition
 delete from utrtest;
@@ -7993,50 +7965,45 @@ insert into utrtest values (3, 'xyzzy');
 -- with a direct modification plan
 explain (verbose, costs off)
 update utrtest set a = 3 returning *;
-                           QUERY PLAN                            
------------------------------------------------------------------
+                                QUERY PLAN                                 
+---------------------------------------------------------------------------
  Update on public.utrtest
    Output: utrtest_1.a, utrtest_1.b
    Update on public.locp utrtest_1
    Foreign Update on public.remp utrtest_2
-   ->  Seq Scan on public.locp utrtest_1
-         Output: 3, utrtest_1.ctid
-   ->  Foreign Update on public.remp utrtest_2
-         Remote SQL: UPDATE public.loct SET a = 3 RETURNING a, b
-(8 rows)
+   ->  Append
+         ->  Seq Scan on public.locp utrtest_1
+               Output: 3, utrtest_1.tableoid, utrtest_1.ctid, NULL::record
+         ->  Foreign Update on public.remp utrtest_2
+               Remote SQL: UPDATE public.loct SET a = 3 RETURNING a, b
+(9 rows)
 
 update utrtest set a = 3 returning *; -- ERROR
 ERROR:  cannot route tuples into foreign table to be updated "remp"
 -- with a non-direct modification plan
 explain (verbose, costs off)
 update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *;
-                                    QUERY PLAN                                    
-----------------------------------------------------------------------------------
+                                             QUERY PLAN                                              
+-----------------------------------------------------------------------------------------------------
  Update on public.utrtest
    Output: utrtest_1.a, utrtest_1.b, "*VALUES*".column1
    Update on public.locp utrtest_1
    Foreign Update on public.remp utrtest_2
      Remote SQL: UPDATE public.loct SET a = $2 WHERE ctid = $1 RETURNING a, b
    ->  Hash Join
-         Output: 3, utrtest_1.ctid, "*VALUES*".*, "*VALUES*".column1
-         Hash Cond: (utrtest_1.a = "*VALUES*".column1)
-         ->  Seq Scan on public.locp utrtest_1
-               Output: utrtest_1.ctid, utrtest_1.a
-         ->  Hash
-               Output: "*VALUES*".*, "*VALUES*".column1
-               ->  Values Scan on "*VALUES*"
-                     Output: "*VALUES*".*, "*VALUES*".column1
-   ->  Hash Join
-         Output: 3, utrtest_2.ctid, utrtest_2.*, "*VALUES*".*, "*VALUES*".column1
-         Hash Cond: (utrtest_2.a = "*VALUES*".column1)
-         ->  Foreign Scan on public.remp utrtest_2
-               Output: utrtest_2.ctid, utrtest_2.*, utrtest_2.a
-               Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE
+         Output: 3, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, (NULL::record)
+         Hash Cond: (utrtest.a = "*VALUES*".column1)
+         ->  Append
+               ->  Seq Scan on public.locp utrtest_1
+                     Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, NULL::record
+               ->  Foreign Scan on public.remp utrtest_2
+                     Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, utrtest_2.*
+                     Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE
          ->  Hash
                Output: "*VALUES*".*, "*VALUES*".column1
                ->  Values Scan on "*VALUES*"
                      Output: "*VALUES*".*, "*VALUES*".column1
-(24 rows)
+(18 rows)
 
 update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *; -- ERROR
 ERROR:  cannot route tuples into foreign table to be updated "remp"
@@ -9423,11 +9390,12 @@ CREATE TABLE batch_cp_up_test1 PARTITION OF batch_cp_upd_test
 INSERT INTO batch_cp_upd_test VALUES (1), (2);
 -- The following moves a row from the local partition to the foreign one
 UPDATE batch_cp_upd_test t SET a = 1 FROM (VALUES (1), (2)) s(a) WHERE t.a = s.a;
+ERROR:  cannot route tuples into foreign table to be updated "batch_cp_upd_test1_f"
 SELECT tableoid::regclass, * FROM batch_cp_upd_test;
        tableoid       | a 
 ----------------------+---
  batch_cp_upd_test1_f | 1
- batch_cp_upd_test1_f | 1
+ batch_cp_up_test1    | 2
 (2 rows)
 
 -- Clean up
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6ba6786c8b..348f7d8d65 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -26,6 +26,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "optimizer/appendinfo.h"
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
 #include "optimizer/optimizer.h"
@@ -337,7 +338,8 @@ static void postgresBeginForeignScan(ForeignScanState *node, int eflags);
 static TupleTableSlot *postgresIterateForeignScan(ForeignScanState *node);
 static void postgresReScanForeignScan(ForeignScanState *node);
 static void postgresEndForeignScan(ForeignScanState *node);
-static void postgresAddForeignUpdateTargets(Query *parsetree,
+static void postgresAddForeignUpdateTargets(PlannerInfo *root,
+											Index rtindex,
 											RangeTblEntry *target_rte,
 											Relation target_relation);
 static List *postgresPlanForeignModify(PlannerInfo *root,
@@ -1637,36 +1639,27 @@ postgresEndForeignScan(ForeignScanState *node)
  *		Add resjunk column(s) needed for update/delete on a foreign table
  */
 static void
-postgresAddForeignUpdateTargets(Query *parsetree,
+postgresAddForeignUpdateTargets(PlannerInfo *root,
+								Index rtindex,
 								RangeTblEntry *target_rte,
 								Relation target_relation)
 {
 	Var		   *var;
-	const char *attrname;
-	TargetEntry *tle;
 
 	/*
 	 * In postgres_fdw, what we need is the ctid, same as for a regular table.
 	 */
 
 	/* Make a Var representing the desired value */
-	var = makeVar(parsetree->resultRelation,
+	var = makeVar(rtindex,
 				  SelfItemPointerAttributeNumber,
 				  TIDOID,
 				  -1,
 				  InvalidOid,
 				  0);
 
-	/* Wrap it in a resjunk TLE with the right name ... */
-	attrname = "ctid";
-
-	tle = makeTargetEntry((Expr *) var,
-						  list_length(parsetree->targetList) + 1,
-						  pstrdup(attrname),
-						  true);
-
-	/* ... and add it to the query's targetlist */
-	parsetree->targetList = lappend(parsetree->targetList, tle);
+	/* Register it as a row-identity column needed by this target rel */
+	add_row_identity_var(root, var, rtindex, "ctid");
 }
 
 /*
@@ -1854,7 +1847,7 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 									rte,
 									resultRelInfo,
 									mtstate->operation,
-									mtstate->mt_plans[subplan_index]->plan,
+									outerPlanState(mtstate)->plan,
 									query,
 									target_attrs,
 									values_end_len,
@@ -2054,8 +2047,7 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 	 */
 	if (plan && plan->operation == CMD_UPDATE &&
 		(resultRelInfo->ri_usesFdwDirectModify ||
-		 resultRelInfo->ri_FdwState) &&
-		resultRelInfo > mtstate->resultRelInfo + mtstate->mt_whichplan)
+		 resultRelInfo->ri_FdwState))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot route tuples into foreign table to be updated \"%s\"",
@@ -2251,6 +2243,65 @@ postgresRecheckForeignScan(ForeignScanState *node, TupleTableSlot *slot)
 	return true;
 }
 
+/*
+ * find_modifytable_subplan
+ *		Helper routine for postgresPlanDirectModify to find the
+ *		ModifyTable subplan node that scans the specified RTI.
+ *
+ * Returns NULL if the subplan couldn't be identified.  That's not a fatal
+ * error condition, we just abandon trying to do the update directly.
+ */
+static ForeignScan *
+find_modifytable_subplan(PlannerInfo *root,
+						 ModifyTable *plan,
+						 Index rtindex,
+						 int subplan_index)
+{
+	Plan	   *subplan = outerPlan(plan);
+
+	/*
+	 * The cases we support are (1) the desired ForeignScan is the immediate
+	 * child of ModifyTable, or (2) it is the subplan_index'th child of an
+	 * Append node that is the immediate child of ModifyTable.  There is no
+	 * point in looking further down, as that would mean that local joins are
+	 * involved, so we can't do the update directly.
+	 *
+	 * There could be a Result atop the Append too, acting to compute the
+	 * UPDATE targetlist values.  We ignore that here; the tlist will be
+	 * checked by our caller.
+	 *
+	 * In principle we could examine all the children of Append etc, but it's
+	 * currently unlikely that the core planner would generate such a plan
+	 * with the children out-of-order.  Moreover, such a search risks costing
+	 * O(N^2) time when there are a lot of children.
+	 */
+	if (IsA(subplan, Append))
+	{
+		Append	   *appendplan = (Append *) subplan;
+
+		if (subplan_index < list_length(appendplan->appendplans))
+			subplan = (Plan *) list_nth(appendplan->appendplans, subplan_index);
+	}
+	else if (IsA(subplan, Result) && IsA(outerPlan(subplan), Append))
+	{
+		Append	   *appendplan = (Append *) outerPlan(subplan);
+
+		if (subplan_index < list_length(appendplan->appendplans))
+			subplan = (Plan *) list_nth(appendplan->appendplans, subplan_index);
+	}
+
+	/* Now, have we got a ForeignScan on the desired rel? */
+	if (IsA(subplan, ForeignScan))
+	{
+		ForeignScan *fscan = (ForeignScan *) subplan;
+
+		if (bms_is_member(rtindex, fscan->fs_relids))
+			return fscan;
+	}
+
+	return NULL;
+}
+
 /*
  * postgresPlanDirectModify
  *		Consider a direct foreign table modification
@@ -2265,13 +2316,13 @@ postgresPlanDirectModify(PlannerInfo *root,
 						 int subplan_index)
 {
 	CmdType		operation = plan->operation;
-	Plan	   *subplan;
 	RelOptInfo *foreignrel;
 	RangeTblEntry *rte;
 	PgFdwRelationInfo *fpinfo;
 	Relation	rel;
 	StringInfoData sql;
 	ForeignScan *fscan;
+	List	   *processed_tlist = NIL;
 	List	   *targetAttrs = NIL;
 	List	   *remote_exprs;
 	List	   *params_list = NIL;
@@ -2289,19 +2340,17 @@ postgresPlanDirectModify(PlannerInfo *root,
 		return false;
 
 	/*
-	 * It's unsafe to modify a foreign table directly if there are any local
-	 * joins needed.
+	 * Try to locate the ForeignScan subplan that's scanning resultRelation.
 	 */
-	subplan = (Plan *) list_nth(plan->plans, subplan_index);
-	if (!IsA(subplan, ForeignScan))
+	fscan = find_modifytable_subplan(root, plan, resultRelation, subplan_index);
+	if (!fscan)
 		return false;
-	fscan = (ForeignScan *) subplan;
 
 	/*
 	 * It's unsafe to modify a foreign table directly if there are any quals
 	 * that should be evaluated locally.
 	 */
-	if (subplan->qual != NIL)
+	if (fscan->scan.plan.qual != NIL)
 		return false;
 
 	/* Safe to fetch data about the target foreign rel */
@@ -2322,14 +2371,16 @@ postgresPlanDirectModify(PlannerInfo *root,
 	 */
 	if (operation == CMD_UPDATE)
 	{
-		ListCell *lc, *lc2;
+		ListCell   *lc,
+				   *lc2;
 
 		/*
-		 * The expressions of concern are the first N columns of the subplan
-		 * targetlist, where N is the length of root->update_colnos.
+		 * The expressions of concern are the first N columns of the processed
+		 * targetlist, where N is the length of the rel's update_colnos.
 		 */
-		targetAttrs = root->update_colnos;
-		forboth(lc, subplan->targetlist, lc2, targetAttrs)
+		get_translated_update_targetlist(root, resultRelation,
+										 &processed_tlist, &targetAttrs);
+		forboth(lc, processed_tlist, lc2, targetAttrs)
 		{
 			TargetEntry *tle = lfirst_node(TargetEntry, lc);
 			AttrNumber attno = lfirst_int(lc2);
@@ -2392,7 +2443,7 @@ postgresPlanDirectModify(PlannerInfo *root,
 		case CMD_UPDATE:
 			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
 								   foreignrel,
-								   ((Plan *) fscan)->targetlist,
+								   processed_tlist,
 								   targetAttrs,
 								   remote_exprs, &params_list,
 								   returningList, &retrieved_attrs);
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 6989957d50..71504791f3 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -424,7 +424,8 @@ GetForeignUpperPaths(PlannerInfo *root,
     <para>
 <programlisting>
 void
-AddForeignUpdateTargets(Query *parsetree,
+AddForeignUpdateTargets(PlannerInfo *root,
+                        Index rtindex,
                         RangeTblEntry *target_rte,
                         Relation target_relation);
 </programlisting>
@@ -440,27 +441,31 @@ AddForeignUpdateTargets(Query *parsetree,
     </para>
 
     <para>
-     To do that, add <structname>TargetEntry</structname> items to
-     <literal>parsetree-&gt;targetList</literal>, containing expressions for the
-     extra values to be fetched.  Each such entry must be marked
-     <structfield>resjunk</structfield> = <literal>true</literal>, and must have a distinct
-     <structfield>resname</structfield> that will identify it at execution time.
-     Avoid using names matching <literal>ctid<replaceable>N</replaceable></literal>,
-     <literal>wholerow</literal>, or
-     <literal>wholerow<replaceable>N</replaceable></literal>, as the core system can
-     generate junk columns of these names.
-     If the extra expressions are more complex than simple Vars, they
-     must be run through <function>eval_const_expressions</function>
-     before adding them to the target list.
-    </para>
-
-    <para>
-     Although this function is called during planning, the
-     information provided is a bit different from that available to other
-     planning routines.
-     <literal>parsetree</literal> is the parse tree for the <command>UPDATE</command> or
-     <command>DELETE</command> command, while <literal>target_rte</literal> and
-     <literal>target_relation</literal> describe the target foreign table.
+     To do that, construct a <structname>Var</structname> representing
+     an extra value you need, and pass it
+     to <function>add_row_identity_var</function>, along with a name for
+     the junk column.  (You can do this more than once if several columns
+     are needed.)  You must choose a distinct junk column name for each
+     different <structname>Var</structname> you need, except
+     that <structname>Var</structname>s that are identical except for
+     the <structfield>varno</structfield> field can and should share a
+     column name.
+     The core system uses the junk column names
+     <literal>tableoid</literal> for a
+     table's <structfield>tableoid</structfield> column,
+     <literal>ctid</literal>
+     or <literal>ctid<replaceable>N</replaceable></literal>
+     for <structfield>ctid</structfield>,
+     <literal>wholerow</literal>
+     for a whole-row <structname>Var</structname> marked with
+     <structfield>vartype</structfield> = <type>RECORD</type>,
+     and <literal>wholerow<replaceable>N</replaceable></literal>
+     for a whole-row <structname>Var</structname> with 
+     <structfield>vartype</structfield> equal to the table's declared rowtype.
+     Re-use these names when you can (the planner will combine duplicate
+     requests for identical junk columns).  If you need another kind of
+     junk column besides these, it might be wise to choose a name prefixed
+     with your extension name, to avoid conflicts against other FDWs.
     </para>
 
     <para>
@@ -495,8 +500,8 @@ PlanForeignModify(PlannerInfo *root,
      <literal>resultRelation</literal> identifies the target foreign table by its
      range table index.  <literal>subplan_index</literal> identifies which target of
      the <structname>ModifyTable</structname> plan node this is, counting from zero;
-     use this if you want to index into <literal>plan-&gt;plans</literal> or other
-     substructure of the <literal>plan</literal> node.
+     use this if you want to index into per-target-relation substructures of the
+     <literal>plan</literal> node.
     </para>
 
     <para>
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index 07aa25799d..4942dbfc18 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -78,7 +78,7 @@
   invoked by <command>UPDATE</command> statements executed on partitioned
   tables, but it currently does not handle the case where a remote partition
   chosen to insert a moved row into is also an <command>UPDATE</command>
-  target partition that will be updated later.
+  target partition that will be updated elsewhere in the same command.
  </para>
 
  <para>
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 2ed696d429..74dbb709fe 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -666,6 +666,7 @@ CopyFrom(CopyFromState cstate)
 	mtstate->ps.plan = NULL;
 	mtstate->ps.state = estate;
 	mtstate->operation = CMD_INSERT;
+	mtstate->mt_nrels = 1;
 	mtstate->resultRelInfo = resultRelInfo;
 	mtstate->rootResultRelInfo = resultRelInfo;
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index afc45429ba..0b1808d503 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2078,7 +2078,6 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	haschildren = planstate->initPlan ||
 		outerPlanState(planstate) ||
 		innerPlanState(planstate) ||
-		IsA(plan, ModifyTable) ||
 		IsA(plan, Append) ||
 		IsA(plan, MergeAppend) ||
 		IsA(plan, BitmapAnd) ||
@@ -2111,11 +2110,6 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* special child plans */
 	switch (nodeTag(plan))
 	{
-		case T_ModifyTable:
-			ExplainMemberNodes(((ModifyTableState *) planstate)->mt_plans,
-							   ((ModifyTableState *) planstate)->mt_nplans,
-							   ancestors, es);
-			break;
 		case T_Append:
 			ExplainMemberNodes(((AppendState *) planstate)->appendplans,
 							   ((AppendState *) planstate)->as_nplans,
@@ -3715,14 +3709,14 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 	}
 
 	/* Should we explicitly label target relations? */
-	labeltargets = (mtstate->mt_nplans > 1 ||
-					(mtstate->mt_nplans == 1 &&
+	labeltargets = (mtstate->mt_nrels > 1 ||
+					(mtstate->mt_nrels == 1 &&
 					 mtstate->resultRelInfo[0].ri_RangeTableIndex != node->nominalRelation));
 
 	if (labeltargets)
 		ExplainOpenGroup("Target Tables", "Target Tables", false, es);
 
-	for (j = 0; j < mtstate->mt_nplans; j++)
+	for (j = 0; j < mtstate->mt_nrels; j++)
 	{
 		ResultRelInfo *resultRelInfo = mtstate->resultRelInfo + j;
 		FdwRoutine *fdwroutine = resultRelInfo->ri_FdwRoutine;
@@ -3817,10 +3811,10 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 			double		insert_path;
 			double		other_path;
 
-			InstrEndLoop(mtstate->mt_plans[0]->instrument);
+			InstrEndLoop(outerPlanState(mtstate)->instrument);
 
 			/* count the number of source rows */
-			total = mtstate->mt_plans[0]->instrument->ntuples;
+			total = outerPlanState(mtstate)->instrument->ntuples;
 			other_path = mtstate->ps.instrument->ntuples2;
 			insert_path = total - other_path;
 
@@ -3836,7 +3830,7 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 }
 
 /*
- * Explain the constituent plans of a ModifyTable, Append, MergeAppend,
+ * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
  *
  * The ancestors list should already contain the immediate parent of these
diff --git a/src/backend/executor/README b/src/backend/executor/README
index 18b2ac1865..4958452730 100644
--- a/src/backend/executor/README
+++ b/src/backend/executor/README
@@ -32,10 +32,14 @@ includes a RETURNING clause, the ModifyTable node delivers the computed
 RETURNING rows as output, otherwise it returns nothing.  Handling INSERT
 is pretty straightforward: the tuples returned from the plan tree below
 ModifyTable are inserted into the correct result relation.  For UPDATE,
-the plan tree returns the computed tuples to be updated, plus a "junk"
-(hidden) CTID column identifying which table row is to be replaced by each
-one.  For DELETE, the plan tree need only deliver a CTID column, and the
-ModifyTable node visits each of those rows and marks the row deleted.
+the plan tree returns the new values of the updated columns, plus "junk"
+(hidden) column(s) identifying which table row is to be updated.  The
+ModifyTable node must fetch that row to extract values for the unchanged
+columns, combine the values into a new row, and apply the update.  (For a
+heap table, the row-identity junk column is a CTID, but other things may
+be used for other table types.)  For DELETE, the plan tree need only deliver
+junk row-identity column(s), and the ModifyTable node visits each of those
+rows and marks the row deleted.
 
 XXX a great deal more documentation needs to be written here...
 
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index ea1530e032..163242f54e 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2416,7 +2416,8 @@ EvalPlanQualInit(EPQState *epqstate, EState *parentestate,
 /*
  * EvalPlanQualSetPlan -- set or change subplan of an EPQState.
  *
- * We need this so that ModifyTable can deal with multiple subplans.
+ * We used to need this so that ModifyTable could deal with multiple subplans.
+ * It could now be refactored out of existence.
  */
 void
 EvalPlanQualSetPlan(EPQState *epqstate, Plan *subplan, List *auxrowmarks)
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 619aaffae4..558060e080 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -82,7 +82,7 @@
  *
  * subplan_resultrel_htab
  *		Hash table to store subplan ResultRelInfos by Oid.  This is used to
- *		cache ResultRelInfos from subplans of an UPDATE ModifyTable node;
+ *		cache ResultRelInfos from targets of an UPDATE ModifyTable node;
  *		NULL in other cases.  Some of these may be useful for tuple routing
  *		to save having to build duplicates.
  *
@@ -527,12 +527,12 @@ ExecHashSubPlanResultRelsByOid(ModifyTableState *mtstate,
 	ctl.entrysize = sizeof(SubplanResultRelHashElem);
 	ctl.hcxt = CurrentMemoryContext;
 
-	htab = hash_create("PartitionTupleRouting table", mtstate->mt_nplans,
+	htab = hash_create("PartitionTupleRouting table", mtstate->mt_nrels,
 					   &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
 	proute->subplan_resultrel_htab = htab;
 
 	/* Hash all subplans by their Oid */
-	for (i = 0; i < mtstate->mt_nplans; i++)
+	for (i = 0; i < mtstate->mt_nrels; i++)
 	{
 		ResultRelInfo *rri = &mtstate->resultRelInfo[i];
 		bool		found;
@@ -628,10 +628,10 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 		 */
 		Assert((node->operation == CMD_INSERT &&
 				list_length(node->withCheckOptionLists) == 1 &&
-				list_length(node->plans) == 1) ||
+				list_length(node->resultRelations) == 1) ||
 			   (node->operation == CMD_UPDATE &&
 				list_length(node->withCheckOptionLists) ==
-				list_length(node->plans)));
+				list_length(node->resultRelations)));
 
 		/*
 		 * Use the WCO list of the first plan as a reference to calculate
@@ -687,10 +687,10 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 		/* See the comment above for WCO lists. */
 		Assert((node->operation == CMD_INSERT &&
 				list_length(node->returningLists) == 1 &&
-				list_length(node->plans) == 1) ||
+				list_length(node->resultRelations) == 1) ||
 			   (node->operation == CMD_UPDATE &&
 				list_length(node->returningLists) ==
-				list_length(node->plans)));
+				list_length(node->resultRelations)));
 
 		/*
 		 * Use the RETURNING list of the first plan as a reference to
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index b9064bfe66..739ca8384e 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -19,14 +19,10 @@
  *		ExecReScanModifyTable - rescan the ModifyTable node
  *
  *	 NOTES
- *		Each ModifyTable node contains a list of one or more subplans,
- *		much like an Append node.  There is one subplan per result relation.
- *		The key reason for this is that in an inherited UPDATE command, each
- *		result relation could have a different schema (more or different
- *		columns) requiring a different plan tree to produce it.  In an
- *		inherited DELETE, all the subplans should produce the same output
- *		rowtype, but we might still find that different plans are appropriate
- *		for different child relations.
+ *		The ModifyTable node receives input from its outerPlan, which is
+ *		the data to insert for INSERT cases, or the changed columns' new
+ *		values plus row-locating info for UPDATE cases, or just the
+ *		row-locating info for DELETE cases.
  *
  *		If the query specifies RETURNING, then the ModifyTable returns a
  *		RETURNING tuple after completing each row insert, update, or delete.
@@ -58,6 +54,12 @@
 #include "utils/rel.h"
 
 
+typedef struct MTTargetRelLookup
+{
+	Oid			relationOid;	/* hash key, must be first */
+	int			relationIndex;	/* rel's index in resultRelInfo[] array */
+} MTTargetRelLookup;
+
 static void ExecBatchInsert(ModifyTableState *mtstate,
 								 ResultRelInfo *resultRelInfo,
 								 TupleTableSlot **slots,
@@ -372,21 +374,39 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
 /*
  * ExecGetInsertNewTuple
  *		This prepares a "new" tuple ready to be inserted into given result
- *		relation by removing any junk columns of the plan's output tuple.
- *
- * Note: currently, this is really dead code, because INSERT cases don't
- * receive any junk columns so there's never a projection to be done.
+ *		relation, by removing any junk columns of the plan's output tuple
+ *		and (if necessary) coercing the tuple to the right tuple format.
  */
 static TupleTableSlot *
 ExecGetInsertNewTuple(ResultRelInfo *relinfo,
 					  TupleTableSlot *planSlot)
 {
 	ProjectionInfo *newProj = relinfo->ri_projectNew;
-	ExprContext   *econtext;
+	ExprContext *econtext;
 
+	/*
+	 * If there's no projection to be done, just make sure the slot is of the
+	 * right type for the target rel.  If the planSlot is the right type we
+	 * can use it as-is, else copy the data into ri_newTupleSlot.
+	 */
 	if (newProj == NULL)
-		return planSlot;
+	{
+		if (relinfo->ri_newTupleSlot->tts_ops != planSlot->tts_ops)
+		{
+			ExecCopySlot(relinfo->ri_newTupleSlot, planSlot);
+			return relinfo->ri_newTupleSlot;
+		}
+		else
+			return planSlot;
+	}
 
+	/*
+	 * Else project; since the projection output slot is ri_newTupleSlot, this
+	 * will also fix any slot-type problem.
+	 *
+	 * Note: currently, this is dead code, because INSERT cases don't receive
+	 * any junk columns so there's never a projection to be done.
+	 */
 	econtext = newProj->pi_exprContext;
 	econtext->ecxt_outertuple = planSlot;
 	return ExecProject(newProj);
@@ -396,8 +416,10 @@ ExecGetInsertNewTuple(ResultRelInfo *relinfo,
  * ExecGetUpdateNewTuple
  *		This prepares a "new" tuple by combining an UPDATE subplan's output
  *		tuple (which contains values of changed columns) with unchanged
- *		columns taken from the old tuple.  The subplan tuple might also
- *		contain junk columns, which are ignored.
+ *		columns taken from the old tuple.
+ *
+ * The subplan tuple might also contain junk columns, which are ignored.
+ * Note that the projection also ensures we have a slot of the right type.
  */
 TupleTableSlot *
 ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
@@ -405,9 +427,8 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
 					  TupleTableSlot *oldSlot)
 {
 	ProjectionInfo *newProj = relinfo->ri_projectNew;
-	ExprContext   *econtext;
+	ExprContext *econtext;
 
-	Assert(newProj != NULL);
 	Assert(planSlot != NULL && !TTS_EMPTY(planSlot));
 	Assert(oldSlot != NULL && !TTS_EMPTY(oldSlot));
 
@@ -1249,9 +1270,7 @@ static bool
 ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 						 ResultRelInfo *resultRelInfo,
 						 ItemPointer tupleid, HeapTuple oldtuple,
-						 TupleTableSlot *slot,
-						 TupleTableSlot *oldSlot,
-						 TupleTableSlot *planSlot,
+						 TupleTableSlot *slot, TupleTableSlot *planSlot,
 						 EPQState *epqstate, bool canSetTag,
 						 TupleTableSlot **retry_slot,
 						 TupleTableSlot **inserted_tuple)
@@ -1327,7 +1346,8 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 		else
 		{
 			/* Fetch the most recent version of old tuple. */
-			ExecClearTuple(oldSlot);
+			TupleTableSlot *oldSlot = resultRelInfo->ri_oldTupleSlot;
+
 			if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
 											   tupleid,
 											   SnapshotAny,
@@ -1340,7 +1360,7 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 	}
 
 	/*
-	 * resultRelInfo is one of the per-subplan resultRelInfos.  So we should
+	 * resultRelInfo is one of the per-relation resultRelInfos.  So we should
 	 * convert the tuple into root's tuple descriptor if needed, since
 	 * ExecInsert() starts the search from root.
 	 */
@@ -1384,10 +1404,10 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
  *		foreign table triggers; it is NULL when the foreign table has
  *		no relevant triggers.
  *
- *		slot contains the new tuple value to be stored, while oldSlot
- *		contains the old tuple being replaced.  planSlot is the output
- *		of the ModifyTable's subplan; we use it to access values from
- *		other input tables (for RETURNING), row-ID junk columns, etc.
+ *		slot contains the new tuple value to be stored.
+ *		planSlot is the output of the ModifyTable's subplan; we use it
+ *		to access values from other input tables (for RETURNING),
+ *		row-ID junk columns, etc.
  *
  *		Returns RETURNING result if any, otherwise NULL.
  * ----------------------------------------------------------------
@@ -1398,7 +1418,6 @@ ExecUpdate(ModifyTableState *mtstate,
 		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
-		   TupleTableSlot *oldSlot,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
 		   EState *estate,
@@ -1536,8 +1555,8 @@ lreplace:;
 			 * the tuple we're trying to move has been concurrently updated.
 			 */
 			retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
-											  oldtuple, slot, oldSlot,
-											  planSlot, epqstate, canSetTag,
+											  oldtuple, slot, planSlot,
+											  epqstate, canSetTag,
 											  &retry_slot, &inserted_tuple);
 			if (retry)
 			{
@@ -1616,6 +1635,7 @@ lreplace:;
 				{
 					TupleTableSlot *inputslot;
 					TupleTableSlot *epqslot;
+					TupleTableSlot *oldSlot;
 
 					if (IsolationUsesXactSnapshot())
 						ereport(ERROR,
@@ -1650,7 +1670,7 @@ lreplace:;
 								return NULL;
 
 							/* Fetch the most recent version of old tuple. */
-							ExecClearTuple(oldSlot);
+							oldSlot = resultRelInfo->ri_oldTupleSlot;
 							if (!table_tuple_fetch_row_version(resultRelationDesc,
 															   tupleid,
 															   SnapshotAny,
@@ -1953,7 +1973,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
 	/* Execute UPDATE with projection */
 	*returning = ExecUpdate(mtstate, resultRelInfo, conflictTid, NULL,
 							resultRelInfo->ri_onConflict->oc_ProjSlot,
-							existing, planSlot,
+							planSlot,
 							&mtstate->mt_epqstate, mtstate->ps.state,
 							canSetTag);
 
@@ -2132,6 +2152,7 @@ ExecModifyTable(PlanState *pstate)
 	PlanState  *subplanstate;
 	TupleTableSlot *slot;
 	TupleTableSlot *planSlot;
+	TupleTableSlot *oldSlot;
 	ItemPointer tupleid;
 	ItemPointerData tuple_ctid;
 	HeapTupleData oldtupdata;
@@ -2173,11 +2194,11 @@ ExecModifyTable(PlanState *pstate)
 	}
 
 	/* Preload local variables */
-	resultRelInfo = node->resultRelInfo + node->mt_whichplan;
-	subplanstate = node->mt_plans[node->mt_whichplan];
+	resultRelInfo = node->resultRelInfo + node->mt_lastResultIndex;
+	subplanstate = outerPlanState(node);
 
 	/*
-	 * Fetch rows from subplan(s), and execute the required table modification
+	 * Fetch rows from subplan, and execute the required table modification
 	 * for each row.
 	 */
 	for (;;)
@@ -2200,29 +2221,61 @@ ExecModifyTable(PlanState *pstate)
 
 		planSlot = ExecProcNode(subplanstate);
 
+		/* No more tuples to process? */
 		if (TupIsNull(planSlot))
-		{
-			/* advance to next subplan if any */
-			node->mt_whichplan++;
-			if (node->mt_whichplan < node->mt_nplans)
-			{
-				resultRelInfo++;
-				subplanstate = node->mt_plans[node->mt_whichplan];
-				EvalPlanQualSetPlan(&node->mt_epqstate, subplanstate->plan,
-									node->mt_arowmarks[node->mt_whichplan]);
-				continue;
-			}
-			else
-				break;
-		}
+			break;
 
 		/*
-		 * Ensure input tuple is the right format for the target relation.
+		 * When there are multiple result relations, each tuple contains a
+		 * junk column that gives the OID of the rel from which it came.
+		 * Extract it and select the correct result relation.
 		 */
-		if (node->mt_scans[node->mt_whichplan]->tts_ops != planSlot->tts_ops)
+		if (AttributeNumberIsValid(node->mt_resultOidAttno))
 		{
-			ExecCopySlot(node->mt_scans[node->mt_whichplan], planSlot);
-			planSlot = node->mt_scans[node->mt_whichplan];
+			Datum		datum;
+			bool		isNull;
+			Oid			resultoid;
+
+			datum = ExecGetJunkAttribute(planSlot, node->mt_resultOidAttno,
+										 &isNull);
+			if (isNull)
+				elog(ERROR, "tableoid is NULL");
+			resultoid = DatumGetObjectId(datum);
+
+			/* If it's not the same as last time, we need to locate the rel */
+			if (resultoid != node->mt_lastResultOid)
+			{
+				if (node->mt_resultOidHash)
+				{
+					/* Use the pre-built hash table to locate the rel */
+					MTTargetRelLookup *mtlookup;
+
+					mtlookup = (MTTargetRelLookup *)
+						hash_search(node->mt_resultOidHash, &resultoid,
+									HASH_FIND, NULL);
+					if (!mtlookup)
+						elog(ERROR, "incorrect result rel OID %u", resultoid);
+					node->mt_lastResultOid = resultoid;
+					node->mt_lastResultIndex = mtlookup->relationIndex;
+					resultRelInfo = node->resultRelInfo + mtlookup->relationIndex;
+				}
+				else
+				{
+					/* With few target rels, just do a simple search */
+					int			ndx;
+
+					for (ndx = 0; ndx < node->mt_nrels; ndx++)
+					{
+						resultRelInfo = node->resultRelInfo + ndx;
+						if (RelationGetRelid(resultRelInfo->ri_RelationDesc) == resultoid)
+							break;
+					}
+					if (ndx >= node->mt_nrels)
+						elog(ERROR, "incorrect result rel OID %u", resultoid);
+					node->mt_lastResultOid = resultoid;
+					node->mt_lastResultIndex = ndx;
+				}
+			}
 		}
 
 		/*
@@ -2333,39 +2386,34 @@ ExecModifyTable(PlanState *pstate)
 								  estate, node->canSetTag);
 				break;
 			case CMD_UPDATE:
+				/*
+				 * Make the new tuple by combining plan's output tuple with
+				 * the old tuple being updated.
+				 */
+				oldSlot = resultRelInfo->ri_oldTupleSlot;
+				if (oldtuple != NULL)
 				{
-					TupleTableSlot *oldSlot = resultRelInfo->ri_oldTupleSlot;
-
-					/*
-					 * Make the new tuple by combining plan's output tuple
-					 * with the old tuple being updated.
-					 */
-					ExecClearTuple(oldSlot);
-					if (oldtuple != NULL)
-					{
-						/* Foreign table update, store the wholerow attr. */
-						ExecForceStoreHeapTuple(oldtuple, oldSlot, false);
-					}
-					else
-					{
-						/* Fetch the most recent version of old tuple. */
-						Relation	relation = resultRelInfo->ri_RelationDesc;
-
-						Assert(tupleid != NULL);
-						if (!table_tuple_fetch_row_version(relation, tupleid,
-														   SnapshotAny,
-														   oldSlot))
-							elog(ERROR, "failed to fetch tuple being updated");
-					}
-					slot = ExecGetUpdateNewTuple(resultRelInfo, planSlot,
-												 oldSlot);
-
-					/* Now apply the update. */
-					slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple,
-									  slot, oldSlot, planSlot,
-									  &node->mt_epqstate, estate,
-									  node->canSetTag);
+					/* Use the wholerow junk attr as the old tuple. */
+					ExecForceStoreHeapTuple(oldtuple, oldSlot, false);
 				}
+				else
+				{
+					/* Fetch the most recent version of old tuple. */
+					Relation	relation = resultRelInfo->ri_RelationDesc;
+
+					Assert(tupleid != NULL);
+					if (!table_tuple_fetch_row_version(relation, tupleid,
+													   SnapshotAny,
+													   oldSlot))
+						elog(ERROR, "failed to fetch tuple being updated");
+				}
+				slot = ExecGetUpdateNewTuple(resultRelInfo, planSlot,
+											 oldSlot);
+
+				/* Now apply the update. */
+				slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
+								  planSlot, &node->mt_epqstate, estate,
+								  node->canSetTag);
 				break;
 			case CMD_DELETE:
 				slot = ExecDelete(node, resultRelInfo, tupleid, oldtuple,
@@ -2425,12 +2473,12 @@ ModifyTableState *
 ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 {
 	ModifyTableState *mtstate;
+	Plan	   *subplan = outerPlan(node);
 	CmdType		operation = node->operation;
-	int			nplans = list_length(node->plans);
+	int			nrels = list_length(node->resultRelations);
 	ResultRelInfo *resultRelInfo;
-	Plan	   *subplan;
-	ListCell   *l,
-			   *l1;
+	List	   *arowmarks;
+	ListCell   *l;
 	int			i;
 	Relation	rel;
 	bool		update_tuple_routing_needed = node->partColsUpdated;
@@ -2450,10 +2498,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	mtstate->canSetTag = node->canSetTag;
 	mtstate->mt_done = false;
 
-	mtstate->mt_plans = (PlanState **) palloc0(sizeof(PlanState *) * nplans);
+	mtstate->mt_nrels = nrels;
 	mtstate->resultRelInfo = (ResultRelInfo *)
-		palloc(nplans * sizeof(ResultRelInfo));
-	mtstate->mt_scans = (TupleTableSlot **) palloc0(sizeof(TupleTableSlot *) * nplans);
+		palloc(nrels * sizeof(ResultRelInfo));
 
 	/*----------
 	 * Resolve the target relation. This is the same as:
@@ -2482,9 +2529,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 							   linitial_int(node->resultRelations));
 	}
 
-	mtstate->mt_arowmarks = (List **) palloc0(sizeof(List *) * nplans);
-	mtstate->mt_nplans = nplans;
-
 	/* set up epqstate with dummy subplan data for the moment */
 	EvalPlanQualInit(&mtstate->mt_epqstate, estate, NULL, NIL, node->epqParam);
 	mtstate->fireBSTriggers = true;
@@ -2497,23 +2541,17 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		ExecSetupTransitionCaptureState(mtstate, estate);
 
 	/*
-	 * call ExecInitNode on each of the plans to be executed and save the
-	 * results into the array "mt_plans".  This is also a convenient place to
-	 * verify that the proposed target relations are valid and open their
-	 * indexes for insertion of new index entries.
+	 * Open all the result relations and initialize the ResultRelInfo structs.
+	 * (But root relation was initialized above, if it's part of the array.)
+	 * We must do this before initializing the subplan, because direct-modify
+	 * FDWs expect their ResultRelInfos to be available.
 	 */
 	resultRelInfo = mtstate->resultRelInfo;
 	i = 0;
-	forboth(l, node->resultRelations, l1, node->plans)
+	foreach(l, node->resultRelations)
 	{
 		Index		resultRelation = lfirst_int(l);
 
-		subplan = (Plan *) lfirst(l1);
-
-		/*
-		 * This opens result relation and fills ResultRelInfo. (root relation
-		 * was initialized already.)
-		 */
 		if (resultRelInfo != mtstate->rootResultRelInfo)
 			ExecInitResultRelation(estate, resultRelInfo, resultRelation);
 
@@ -2526,6 +2564,22 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		 */
 		CheckValidResultRel(resultRelInfo, operation);
 
+		resultRelInfo++;
+		i++;
+	}
+
+	/*
+	 * Now we may initialize the subplan.
+	 */
+	outerPlanState(mtstate) = ExecInitNode(subplan, estate, eflags);
+
+	/*
+	 * Do additional per-result-relation initialization.
+	 */
+	for (i = 0; i < nrels; i++)
+	{
+		resultRelInfo = &mtstate->resultRelInfo[i];
+
 		/*
 		 * If there are indices on the result relation, open them and save
 		 * descriptors in the result relation info, so that we can add new
@@ -2551,12 +2605,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			operation == CMD_UPDATE)
 			update_tuple_routing_needed = true;
 
-		/* Now init the plan for this result rel */
-		mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags);
-		mtstate->mt_scans[i] =
-			ExecInitExtraTupleSlot(mtstate->ps.state, ExecGetResultType(mtstate->mt_plans[i]),
-								   table_slot_callbacks(resultRelInfo->ri_RelationDesc));
-
 		/* Also let FDWs init themselves for foreign-table result rels */
 		if (!resultRelInfo->ri_usesFdwDirectModify &&
 			resultRelInfo->ri_FdwRoutine != NULL &&
@@ -2588,11 +2636,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			resultRelInfo->ri_ChildToRootMap =
 				convert_tuples_by_name(RelationGetDescr(resultRelInfo->ri_RelationDesc),
 									   RelationGetDescr(mtstate->rootResultRelInfo->ri_RelationDesc));
-		resultRelInfo++;
-		i++;
 	}
 
-	/* Get the target relation */
+	/* Get the root target relation */
 	rel = mtstate->rootResultRelInfo->ri_RelationDesc;
 
 	/*
@@ -2708,8 +2754,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		TupleDesc	relationDesc;
 		TupleDesc	tupDesc;
 
-		/* insert may only have one plan, inheritance is not expanded */
-		Assert(nplans == 1);
+		/* insert may only have one relation, inheritance is not expanded */
+		Assert(nrels == 1);
 
 		/* already exists if created by RETURNING processing above */
 		if (mtstate->ps.ps_ExprContext == NULL)
@@ -2761,34 +2807,24 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	 * EvalPlanQual mechanism needs to be told about them.  Locate the
 	 * relevant ExecRowMarks.
 	 */
+	arowmarks = NIL;
 	foreach(l, node->rowMarks)
 	{
 		PlanRowMark *rc = lfirst_node(PlanRowMark, l);
 		ExecRowMark *erm;
+		ExecAuxRowMark *aerm;
 
 		/* ignore "parent" rowmarks; they are irrelevant at runtime */
 		if (rc->isParent)
 			continue;
 
-		/* find ExecRowMark (same for all subplans) */
+		/* Find ExecRowMark and build ExecAuxRowMark */
 		erm = ExecFindRowMark(estate, rc->rti, false);
-
-		/* build ExecAuxRowMark for each subplan */
-		for (i = 0; i < nplans; i++)
-		{
-			ExecAuxRowMark *aerm;
-
-			subplan = mtstate->mt_plans[i]->plan;
-			aerm = ExecBuildAuxRowMark(erm, subplan->targetlist);
-			mtstate->mt_arowmarks[i] = lappend(mtstate->mt_arowmarks[i], aerm);
-		}
+		aerm = ExecBuildAuxRowMark(erm, subplan->targetlist);
+		arowmarks = lappend(arowmarks, aerm);
 	}
 
-	/* select first subplan */
-	mtstate->mt_whichplan = 0;
-	subplan = (Plan *) linitial(node->plans);
-	EvalPlanQualSetPlan(&mtstate->mt_epqstate, subplan,
-						mtstate->mt_arowmarks[0]);
+	EvalPlanQualSetPlan(&mtstate->mt_epqstate, subplan, arowmarks);
 
 	/*
 	 * Initialize projection(s) to create tuples suitable for result rel(s).
@@ -2801,15 +2837,14 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	 *
 	 * If there are multiple result relations, each one needs its own
 	 * projection.  Note multiple rels are only possible for UPDATE/DELETE, so
-	 * we can't be fooled by some needing a filter and some not.
+	 * we can't be fooled by some needing a projection and some not.
 	 *
 	 * This section of code is also a convenient place to verify that the
 	 * output of an INSERT or UPDATE matches the target table(s).
 	 */
-	for (i = 0; i < nplans; i++)
+	for (i = 0; i < nrels; i++)
 	{
 		resultRelInfo = &mtstate->resultRelInfo[i];
-		subplan = mtstate->mt_plans[i]->plan;
 
 		/*
 		 * Prepare to generate tuples suitable for the target relation.
@@ -2827,14 +2862,24 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 				else
 					need_projection = true;
 			}
+
+			/*
+			 * The junk-free list must produce a tuple suitable for the result
+			 * relation.
+			 */
+			ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
+								insertTargetList);
+
+			/* We'll need a slot matching the table's format. */
+			resultRelInfo->ri_newTupleSlot =
+				table_slot_create(resultRelInfo->ri_RelationDesc,
+								  &mtstate->ps.state->es_tupleTable);
+
+			/* Build ProjectionInfo if needed (it probably isn't). */
 			if (need_projection)
 			{
 				TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
 
-				resultRelInfo->ri_newTupleSlot =
-					table_slot_create(resultRelInfo->ri_RelationDesc,
-									  &mtstate->ps.state->es_tupleTable);
-
 				/* need an expression context to do the projection */
 				if (mtstate->ps.ps_ExprContext == NULL)
 					ExecAssignExprContext(estate, &mtstate->ps);
@@ -2846,13 +2891,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 											&mtstate->ps,
 											relDesc);
 			}
-
-			/*
-			 * The junk-free list must produce a tuple suitable for the result
-			 * relation.
-			 */
-			ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
-								insertTargetList);
 		}
 		else if (operation == CMD_UPDATE)
 		{
@@ -2863,7 +2901,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 			/*
 			 * For UPDATE, we use the old tuple to fill up missing values in
-			 * the tuple produced by the plan to get the new tuple.
+			 * the tuple produced by the plan to get the new tuple.  We need
+			 * two slots, both matching the table's desired format.
 			 */
 			resultRelInfo->ri_oldTupleSlot =
 				table_slot_create(resultRelInfo->ri_RelationDesc,
@@ -2931,6 +2970,60 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
+	 * If this is an inherited update/delete, there will be a junk attribute
+	 * named "tableoid" present in the subplan's targetlist.  It will be used
+	 * to identify the result relation for a given tuple to be
+	 * updated/deleted.
+	 */
+	mtstate->mt_resultOidAttno =
+		ExecFindJunkAttributeInTlist(subplan->targetlist, "tableoid");
+	Assert(AttributeNumberIsValid(mtstate->mt_resultOidAttno) || nrels == 1);
+	mtstate->mt_lastResultOid = InvalidOid; /* force lookup at first tuple */
+	mtstate->mt_lastResultIndex = 0;	/* must be zero if no such attr */
+
+	/*
+	 * If there are a lot of result relations, use a hash table to speed the
+	 * lookups.  If there are not a lot, a simple linear search is faster.
+	 *
+	 * It's not clear where the threshold is, but try 64 for starters.  In a
+	 * debugging build, use a small threshold so that we get some test
+	 * coverage of both code paths.
+	 */
+#ifdef USE_ASSERT_CHECKING
+#define MT_NRELS_HASH 4
+#else
+#define MT_NRELS_HASH 64
+#endif
+	if (nrels >= MT_NRELS_HASH)
+	{
+		HASHCTL		hash_ctl;
+
+		hash_ctl.keysize = sizeof(Oid);
+		hash_ctl.entrysize = sizeof(MTTargetRelLookup);
+		hash_ctl.hcxt = CurrentMemoryContext;
+		mtstate->mt_resultOidHash =
+			hash_create("ModifyTable target hash",
+						nrels, &hash_ctl,
+						HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+		for (i = 0; i < nrels; i++)
+		{
+			Oid			hashkey;
+			MTTargetRelLookup *mtlookup;
+			bool		found;
+
+			resultRelInfo = &mtstate->resultRelInfo[i];
+			hashkey = RelationGetRelid(resultRelInfo->ri_RelationDesc);
+			mtlookup = (MTTargetRelLookup *)
+				hash_search(mtstate->mt_resultOidHash, &hashkey,
+							HASH_ENTER, &found);
+			Assert(!found);
+			mtlookup->relationIndex = i;
+		}
+	}
+	else
+		mtstate->mt_resultOidHash = NULL;
+
 	/*
 	 * Determine if the FDW supports batch insert and determine the batch
 	 * size (a FDW may support batching, but it may be disabled for the
@@ -2942,7 +3035,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	if (operation == CMD_INSERT)
 	{
 		resultRelInfo = mtstate->resultRelInfo;
-		for (i = 0; i < nplans; i++)
+		for (i = 0; i < nrels; i++)
 		{
 			if (!resultRelInfo->ri_usesFdwDirectModify &&
 				resultRelInfo->ri_FdwRoutine != NULL &&
@@ -2991,7 +3084,7 @@ ExecEndModifyTable(ModifyTableState *node)
 	/*
 	 * Allow any FDWs to shut down
 	 */
-	for (i = 0; i < node->mt_nplans; i++)
+	for (i = 0; i < node->mt_nrels; i++)
 	{
 		ResultRelInfo *resultRelInfo = node->resultRelInfo + i;
 
@@ -3031,10 +3124,9 @@ ExecEndModifyTable(ModifyTableState *node)
 	EvalPlanQualEnd(&node->mt_epqstate);
 
 	/*
-	 * shut down subplans
+	 * shut down subplan
 	 */
-	for (i = 0; i < node->mt_nplans; i++)
-		ExecEndNode(node->mt_plans[i]);
+	ExecEndNode(outerPlanState(node));
 }
 
 void
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 1ec586729b..b99babcf2a 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -206,7 +206,6 @@ _copyModifyTable(const ModifyTable *from)
 	COPY_SCALAR_FIELD(rootRelation);
 	COPY_SCALAR_FIELD(partColsUpdated);
 	COPY_NODE_FIELD(resultRelations);
-	COPY_NODE_FIELD(plans);
 	COPY_NODE_FIELD(updateColnosLists);
 	COPY_NODE_FIELD(withCheckOptionLists);
 	COPY_NODE_FIELD(returningLists);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 38226530c6..860e9a2a06 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4002,12 +4002,6 @@ planstate_tree_walker(PlanState *planstate,
 	/* special child plans */
 	switch (nodeTag(plan))
 	{
-		case T_ModifyTable:
-			if (planstate_walk_members(((ModifyTableState *) planstate)->mt_plans,
-									   ((ModifyTableState *) planstate)->mt_nplans,
-									   walker, context))
-				return true;
-			break;
 		case T_Append:
 			if (planstate_walk_members(((AppendState *) planstate)->appendplans,
 									   ((AppendState *) planstate)->as_nplans,
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 99fb38c05a..a18213ed87 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -407,7 +407,6 @@ _outModifyTable(StringInfo str, const ModifyTable *node)
 	WRITE_UINT_FIELD(rootRelation);
 	WRITE_BOOL_FIELD(partColsUpdated);
 	WRITE_NODE_FIELD(resultRelations);
-	WRITE_NODE_FIELD(plans);
 	WRITE_NODE_FIELD(updateColnosLists);
 	WRITE_NODE_FIELD(withCheckOptionLists);
 	WRITE_NODE_FIELD(returningLists);
@@ -2136,14 +2135,13 @@ _outModifyTablePath(StringInfo str, const ModifyTablePath *node)
 
 	_outPathInfo(str, (const Path *) node);
 
+	WRITE_NODE_FIELD(subpath);
 	WRITE_ENUM_FIELD(operation, CmdType);
 	WRITE_BOOL_FIELD(canSetTag);
 	WRITE_UINT_FIELD(nominalRelation);
 	WRITE_UINT_FIELD(rootRelation);
 	WRITE_BOOL_FIELD(partColsUpdated);
 	WRITE_NODE_FIELD(resultRelations);
-	WRITE_NODE_FIELD(subpaths);
-	WRITE_NODE_FIELD(subroots);
 	WRITE_NODE_FIELD(updateColnosLists);
 	WRITE_NODE_FIELD(withCheckOptionLists);
 	WRITE_NODE_FIELD(returningLists);
@@ -2260,7 +2258,10 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_NODE_FIELD(right_join_clauses);
 	WRITE_NODE_FIELD(full_join_clauses);
 	WRITE_NODE_FIELD(join_info_list);
+	WRITE_BITMAPSET_FIELD(all_result_relids);
+	WRITE_BITMAPSET_FIELD(leaf_result_relids);
 	WRITE_NODE_FIELD(append_rel_list);
+	WRITE_NODE_FIELD(row_identity_vars);
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(placeholder_list);
 	WRITE_NODE_FIELD(fkey_list);
@@ -2573,6 +2574,17 @@ _outAppendRelInfo(StringInfo str, const AppendRelInfo *node)
 	WRITE_OID_FIELD(parent_reloid);
 }
 
+static void
+_outRowIdentityVarInfo(StringInfo str, const RowIdentityVarInfo *node)
+{
+	WRITE_NODE_TYPE("ROWIDENTITYVARINFO");
+
+	WRITE_NODE_FIELD(rowidvar);
+	WRITE_INT_FIELD(rowidwidth);
+	WRITE_STRING_FIELD(rowidname);
+	WRITE_BITMAPSET_FIELD(rowidrels);
+}
+
 static void
 _outPlaceHolderInfo(StringInfo str, const PlaceHolderInfo *node)
 {
@@ -4222,6 +4234,9 @@ outNode(StringInfo str, const void *obj)
 			case T_AppendRelInfo:
 				_outAppendRelInfo(str, obj);
 				break;
+			case T_RowIdentityVarInfo:
+				_outRowIdentityVarInfo(str, obj);
+				break;
 			case T_PlaceHolderInfo:
 				_outPlaceHolderInfo(str, obj);
 				break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 0b6331d3da..f241cc268f 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1682,7 +1682,6 @@ _readModifyTable(void)
 	READ_UINT_FIELD(rootRelation);
 	READ_BOOL_FIELD(partColsUpdated);
 	READ_NODE_FIELD(resultRelations);
-	READ_NODE_FIELD(plans);
 	READ_NODE_FIELD(updateColnosLists);
 	READ_NODE_FIELD(withCheckOptionLists);
 	READ_NODE_FIELD(returningLists);
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index d73ac562eb..078f6572c9 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -1148,7 +1148,7 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 			Var		   *parentvar = (Var *) lfirst(parentvars);
 			Node	   *childvar = (Node *) lfirst(childvars);
 
-			if (IsA(parentvar, Var))
+			if (IsA(parentvar, Var) && parentvar->varno == parentRTindex)
 			{
 				int			pndx = parentvar->varattno - rel->min_attr;
 				int32		child_width = 0;
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index ff536e6b24..aa0bb2bcab 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -3397,7 +3397,7 @@ check_index_predicates(PlannerInfo *root, RelOptInfo *rel)
 	 * and pass them through to EvalPlanQual via a side channel; but for now,
 	 * we just don't remove implied quals at all for target relations.
 	 */
-	is_target_rel = (rel->relid == root->parse->resultRelation ||
+	is_target_rel = (bms_is_member(rel->relid, root->all_result_relids) ||
 					 get_plan_rowmark(root->rowMarks, rel->relid) != NULL);
 
 	/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 4bb482879f..69b4e5a703 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -297,11 +297,11 @@ static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy, Plan *lefttree,
 static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam);
 static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan);
 static ProjectSet *make_project_set(List *tlist, Plan *subplan);
-static ModifyTable *make_modifytable(PlannerInfo *root,
+static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 CmdType operation, bool canSetTag,
 									 Index nominalRelation, Index rootRelation,
 									 bool partColsUpdated,
-									 List *resultRelations, List *subplans, List *subroots,
+									 List *resultRelations,
 									 List *updateColnosLists,
 									 List *withCheckOptionLists, List *returningLists,
 									 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
@@ -2252,12 +2252,7 @@ create_groupingsets_plan(PlannerInfo *root, GroupingSetsPath *best_path)
 	/*
 	 * During setrefs.c, we'll need the grouping_map to fix up the cols lists
 	 * in GroupingFunc nodes.  Save it for setrefs.c to use.
-	 *
-	 * This doesn't work if we're in an inheritance subtree (see notes in
-	 * create_modifytable_plan).  Fortunately we can't be because there would
-	 * never be grouping in an UPDATE/DELETE; but let's Assert that.
 	 */
-	Assert(root->inhTargetKind == INHKIND_NONE);
 	Assert(root->grouping_map == NULL);
 	root->grouping_map = grouping_map;
 
@@ -2419,12 +2414,7 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	 * with InitPlan output params.  (We can't just do that locally in the
 	 * MinMaxAgg node, because path nodes above here may have Agg references
 	 * as well.)  Save the mmaggregates list to tell setrefs.c to do that.
-	 *
-	 * This doesn't work if we're in an inheritance subtree (see notes in
-	 * create_modifytable_plan).  Fortunately we can't be because there would
-	 * never be aggregates in an UPDATE/DELETE; but let's Assert that.
 	 */
-	Assert(root->inhTargetKind == INHKIND_NONE);
 	Assert(root->minmax_aggs == NIL);
 	root->minmax_aggs = best_path->mmaggregates;
 
@@ -2641,44 +2631,23 @@ static ModifyTable *
 create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 {
 	ModifyTable *plan;
-	List	   *subplans = NIL;
-	ListCell   *subpaths,
-			   *subroots,
-			   *lc;
-
-	/* Build the plan for each input path */
-	forboth(subpaths, best_path->subpaths,
-			subroots, best_path->subroots)
-	{
-		Path	   *subpath = (Path *) lfirst(subpaths);
-		PlannerInfo *subroot = (PlannerInfo *) lfirst(subroots);
-		Plan	   *subplan;
+	Path	   *subpath = best_path->subpath;
+	Plan	   *subplan;
 
-		/*
-		 * In an inherited UPDATE/DELETE, reference the per-child modified
-		 * subroot while creating Plans from Paths for the child rel.  This is
-		 * a kluge, but otherwise it's too hard to ensure that Plan creation
-		 * functions (particularly in FDWs) don't depend on the contents of
-		 * "root" matching what they saw at Path creation time.  The main
-		 * downside is that creation functions for Plans that might appear
-		 * below a ModifyTable cannot expect to modify the contents of "root"
-		 * and have it "stick" for subsequent processing such as setrefs.c.
-		 * That's not great, but it seems better than the alternative.
-		 */
-		subplan = create_plan_recurse(subroot, subpath, CP_EXACT_TLIST);
+	/* Subplan must produce exactly the specified tlist */
+	subplan = create_plan_recurse(root, subpath, CP_EXACT_TLIST);
 
-		subplans = lappend(subplans, subplan);
-	}
+	/* Transfer resname/resjunk labeling, too, to keep executor happy */
+	apply_tlist_labeling(subplan->targetlist, root->processed_tlist);
 
 	plan = make_modifytable(root,
+							subplan,
 							best_path->operation,
 							best_path->canSetTag,
 							best_path->nominalRelation,
 							best_path->rootRelation,
 							best_path->partColsUpdated,
 							best_path->resultRelations,
-							subplans,
-							best_path->subroots,
 							best_path->updateColnosLists,
 							best_path->withCheckOptionLists,
 							best_path->returningLists,
@@ -2688,41 +2657,6 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 
 	copy_generic_path_info(&plan->plan, &best_path->path);
 
-	forboth(lc, subplans,
-			subroots, best_path->subroots)
-	{
-		Plan	   *subplan = (Plan *) lfirst(lc);
-		PlannerInfo *subroot = (PlannerInfo *) lfirst(subroots);
-
-		/*
-		 * Fix up the resnos of query's TLEs to make them match their ordinal
-		 * position in the list, which they may not in the case of an UPDATE.
-		 * It's safe to revise that targetlist now, because nothing after this
-		 * point needs those resnos to match target relation's attribute
-		 * numbers.
-		 * XXX - we do this simply because apply_tlist_labeling() asserts that
-		 * resnos in processed_tlist and resnos in subplan targetlist are
-		 * exactly same, but maybe we can just remove the assert?
-		 */
-		if (plan->operation == CMD_UPDATE)
-		{
-			ListCell   *l;
-			AttrNumber	resno = 1;
-
-			foreach(l, subroot->processed_tlist)
-			{
-				TargetEntry *tle = lfirst(l);
-
-				tle = flatCopyTargetEntry(tle);
-				tle->resno = resno++;
-				lfirst(l) = tle;
-			}
-		}
-
-		/* Transfer resname/resjunk labeling, too, to keep executor happy */
-		apply_tlist_labeling(subplan->targetlist, subroot->processed_tlist);
-	}
-
 	return plan;
 }
 
@@ -6910,11 +6844,11 @@ make_project_set(List *tlist,
  *	  Build a ModifyTable plan node
  */
 static ModifyTable *
-make_modifytable(PlannerInfo *root,
+make_modifytable(PlannerInfo *root, Plan *subplan,
 				 CmdType operation, bool canSetTag,
 				 Index nominalRelation, Index rootRelation,
 				 bool partColsUpdated,
-				 List *resultRelations, List *subplans, List *subroots,
+				 List *resultRelations,
 				 List *updateColnosLists,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam)
@@ -6923,11 +6857,8 @@ make_modifytable(PlannerInfo *root,
 	List	   *fdw_private_list;
 	Bitmapset  *direct_modify_plans;
 	ListCell   *lc;
-	ListCell   *lc2;
 	int			i;
 
-	Assert(list_length(resultRelations) == list_length(subplans));
-	Assert(list_length(resultRelations) == list_length(subroots));
 	Assert(operation == CMD_UPDATE ?
 		   list_length(resultRelations) == list_length(updateColnosLists) :
 		   updateColnosLists == NIL);
@@ -6936,7 +6867,7 @@ make_modifytable(PlannerInfo *root,
 	Assert(returningLists == NIL ||
 		   list_length(resultRelations) == list_length(returningLists));
 
-	node->plan.lefttree = NULL;
+	node->plan.lefttree = subplan;
 	node->plan.righttree = NULL;
 	node->plan.qual = NIL;
 	/* setrefs.c will fill in the targetlist, if needed */
@@ -6948,7 +6879,6 @@ make_modifytable(PlannerInfo *root,
 	node->rootRelation = rootRelation;
 	node->partColsUpdated = partColsUpdated;
 	node->resultRelations = resultRelations;
-	node->plans = subplans;
 	if (!onconflict)
 	{
 		node->onConflictAction = ONCONFLICT_NONE;
@@ -6988,10 +6918,9 @@ make_modifytable(PlannerInfo *root,
 	fdw_private_list = NIL;
 	direct_modify_plans = NULL;
 	i = 0;
-	forboth(lc, resultRelations, lc2, subroots)
+	foreach(lc, resultRelations)
 	{
 		Index		rti = lfirst_int(lc);
-		PlannerInfo *subroot = lfirst_node(PlannerInfo, lc2);
 		FdwRoutine *fdwroutine;
 		List	   *fdw_private;
 		bool		direct_modify;
@@ -7003,16 +6932,16 @@ make_modifytable(PlannerInfo *root,
 		 * so it's not a baserel; and there are also corner cases for
 		 * updatable views where the target rel isn't a baserel.)
 		 */
-		if (rti < subroot->simple_rel_array_size &&
-			subroot->simple_rel_array[rti] != NULL)
+		if (rti < root->simple_rel_array_size &&
+			root->simple_rel_array[rti] != NULL)
 		{
-			RelOptInfo *resultRel = subroot->simple_rel_array[rti];
+			RelOptInfo *resultRel = root->simple_rel_array[rti];
 
 			fdwroutine = resultRel->fdwroutine;
 		}
 		else
 		{
-			RangeTblEntry *rte = planner_rt_fetch(rti, subroot);
+			RangeTblEntry *rte = planner_rt_fetch(rti, root);
 
 			Assert(rte->rtekind == RTE_RELATION);
 			if (rte->relkind == RELKIND_FOREIGN_TABLE)
@@ -7035,16 +6964,16 @@ make_modifytable(PlannerInfo *root,
 			fdwroutine->IterateDirectModify != NULL &&
 			fdwroutine->EndDirectModify != NULL &&
 			withCheckOptionLists == NIL &&
-			!has_row_triggers(subroot, rti, operation) &&
-			!has_stored_generated_columns(subroot, rti))
-			direct_modify = fdwroutine->PlanDirectModify(subroot, node, rti, i);
+			!has_row_triggers(root, rti, operation) &&
+			!has_stored_generated_columns(root, rti))
+			direct_modify = fdwroutine->PlanDirectModify(root, node, rti, i);
 		if (direct_modify)
 			direct_modify_plans = bms_add_member(direct_modify_plans, i);
 
 		if (!direct_modify &&
 			fdwroutine != NULL &&
 			fdwroutine->PlanForeignModify != NULL)
-			fdw_private = fdwroutine->PlanForeignModify(subroot, node, rti, i);
+			fdw_private = fdwroutine->PlanForeignModify(root, node, rti, i);
 		else
 			fdw_private = NIL;
 		fdw_private_list = lappend(fdw_private_list, fdw_private);
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index e1a13e20c5..273ac0acf7 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -263,6 +263,13 @@ query_planner(PlannerInfo *root,
 	 */
 	add_other_rels_to_query(root);
 
+	/*
+	 * Distribute any UPDATE/DELETE row identity variables to the target
+	 * relations.  This can't be done till we've finished expansion of
+	 * appendrels.
+	 */
+	distribute_row_identity_vars(root);
+
 	/*
 	 * Ready to do the primary planning.
 	 */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index ccb9166a8e..eb5c3c5092 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -129,9 +129,7 @@ typedef struct
 /* Local functions */
 static Node *preprocess_expression(PlannerInfo *root, Node *expr, int kind);
 static void preprocess_qual_conditions(PlannerInfo *root, Node *jtnode);
-static void inheritance_planner(PlannerInfo *root);
-static void grouping_planner(PlannerInfo *root, bool inheritance_update,
-							 double tuple_fraction);
+static void grouping_planner(PlannerInfo *root, double tuple_fraction);
 static grouping_sets_data *preprocess_grouping_sets(PlannerInfo *root);
 static List *remap_to_groupclause_idx(List *groupClause, List *gsets,
 									  int *tleref_to_colnum_map);
@@ -615,7 +613,11 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	root->multiexpr_params = NIL;
 	root->eq_classes = NIL;
 	root->ec_merging_done = false;
+	root->all_result_relids =
+		parse->resultRelation ? bms_make_singleton(parse->resultRelation) : NULL;
+	root->leaf_result_relids = NULL;	/* we'll find out leaf-ness later */
 	root->append_rel_list = NIL;
+	root->row_identity_vars = NIL;
 	root->rowMarks = NIL;
 	memset(root->upper_rels, 0, sizeof(root->upper_rels));
 	memset(root->upper_targets, 0, sizeof(root->upper_targets));
@@ -624,7 +626,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	root->grouping_map = NULL;
 	root->minmax_aggs = NIL;
 	root->qual_security_level = 0;
-	root->inhTargetKind = INHKIND_NONE;
 	root->hasPseudoConstantQuals = false;
 	root->hasAlternativeSubPlans = false;
 	root->hasRecursion = hasRecursion;
@@ -744,6 +745,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 											list_length(rte->securityQuals));
 	}
 
+	/*
+	 * If we have now verified that the query target relation is
+	 * non-inheriting, mark it as a leaf target.
+	 */
+	if (parse->resultRelation)
+	{
+		RangeTblEntry *rte = rt_fetch(parse->resultRelation, parse->rtable);
+
+		if (!rte->inh)
+			root->leaf_result_relids =
+				bms_make_singleton(parse->resultRelation);
+	}
+
 	/*
 	 * Preprocess RowMark information.  We need to do this after subquery
 	 * pullup, so that all base relations are present.
@@ -1000,14 +1014,9 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		remove_useless_result_rtes(root);
 
 	/*
-	 * Do the main planning.  If we have an inherited target relation, that
-	 * needs special processing, else go straight to grouping_planner.
+	 * Do the main planning.
 	 */
-	if (parse->resultRelation &&
-		rt_fetch(parse->resultRelation, parse->rtable)->inh)
-		inheritance_planner(root);
-	else
-		grouping_planner(root, false, tuple_fraction);
+	grouping_planner(root, tuple_fraction);
 
 	/*
 	 * Capture the set of outer-level param IDs we have access to, for use in
@@ -1181,631 +1190,6 @@ preprocess_phv_expression(PlannerInfo *root, Expr *expr)
 	return (Expr *) preprocess_expression(root, (Node *) expr, EXPRKIND_PHV);
 }
 
-/*
- * inheritance_planner
- *	  Generate Paths in the case where the result relation is an
- *	  inheritance set.
- *
- * We have to handle this case differently from cases where a source relation
- * is an inheritance set. Source inheritance is expanded at the bottom of the
- * plan tree (see allpaths.c), but target inheritance has to be expanded at
- * the top.  The reason is that for UPDATE, each target relation needs a
- * different targetlist matching its own column set.  Fortunately,
- * the UPDATE/DELETE target can never be the nullable side of an outer join,
- * so it's OK to generate the plan this way.
- *
- * Returns nothing; the useful output is in the Paths we attach to
- * the (UPPERREL_FINAL, NULL) upperrel stored in *root.
- *
- * Note that we have not done set_cheapest() on the final rel; it's convenient
- * to leave this to the caller.
- */
-static void
-inheritance_planner(PlannerInfo *root)
-{
-	Query	   *parse = root->parse;
-	int			top_parentRTindex = parse->resultRelation;
-	List	   *select_rtable;
-	List	   *select_appinfos;
-	List	   *child_appinfos;
-	List	   *old_child_rtis;
-	List	   *new_child_rtis;
-	Bitmapset  *subqueryRTindexes;
-	Index		next_subquery_rti;
-	int			nominalRelation = -1;
-	Index		rootRelation = 0;
-	List	   *final_rtable = NIL;
-	List	   *final_rowmarks = NIL;
-	List	   *final_appendrels = NIL;
-	int			save_rel_array_size = 0;
-	RelOptInfo **save_rel_array = NULL;
-	AppendRelInfo **save_append_rel_array = NULL;
-	List	   *subpaths = NIL;
-	List	   *subroots = NIL;
-	List	   *resultRelations = NIL;
-	List	   *updateColnosLists = NIL;
-	List	   *withCheckOptionLists = NIL;
-	List	   *returningLists = NIL;
-	List	   *rowMarks;
-	RelOptInfo *final_rel;
-	ListCell   *lc;
-	ListCell   *lc2;
-	Index		rti;
-	RangeTblEntry *parent_rte;
-	Bitmapset  *parent_relids;
-	Query	  **parent_parses;
-
-	/* Should only get here for UPDATE or DELETE */
-	Assert(parse->commandType == CMD_UPDATE ||
-		   parse->commandType == CMD_DELETE);
-
-	/*
-	 * We generate a modified instance of the original Query for each target
-	 * relation, plan that, and put all the plans into a list that will be
-	 * controlled by a single ModifyTable node.  All the instances share the
-	 * same rangetable, but each instance must have its own set of subquery
-	 * RTEs within the finished rangetable because (1) they are likely to get
-	 * scribbled on during planning, and (2) it's not inconceivable that
-	 * subqueries could get planned differently in different cases.  We need
-	 * not create duplicate copies of other RTE kinds, in particular not the
-	 * target relations, because they don't have either of those issues.  Not
-	 * having to duplicate the target relations is important because doing so
-	 * (1) would result in a rangetable of length O(N^2) for N targets, with
-	 * at least O(N^3) work expended here; and (2) would greatly complicate
-	 * management of the rowMarks list.
-	 *
-	 * To begin with, generate a bitmapset of the relids of the subquery RTEs.
-	 */
-	subqueryRTindexes = NULL;
-	rti = 1;
-	foreach(lc, parse->rtable)
-	{
-		RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
-
-		if (rte->rtekind == RTE_SUBQUERY)
-			subqueryRTindexes = bms_add_member(subqueryRTindexes, rti);
-		rti++;
-	}
-
-	/*
-	 * If the parent RTE is a partitioned table, we should use that as the
-	 * nominal target relation, because the RTEs added for partitioned tables
-	 * (including the root parent) as child members of the inheritance set do
-	 * not appear anywhere else in the plan, so the confusion explained below
-	 * for non-partitioning inheritance cases is not possible.
-	 */
-	parent_rte = rt_fetch(top_parentRTindex, parse->rtable);
-	Assert(parent_rte->inh);
-	if (parent_rte->relkind == RELKIND_PARTITIONED_TABLE)
-	{
-		nominalRelation = top_parentRTindex;
-		rootRelation = top_parentRTindex;
-	}
-
-	/*
-	 * Before generating the real per-child-relation plans, do a cycle of
-	 * planning as though the query were a SELECT.  The objective here is to
-	 * find out which child relations need to be processed, using the same
-	 * expansion and pruning logic as for a SELECT.  We'll then pull out the
-	 * RangeTblEntry-s generated for the child rels, and make use of the
-	 * AppendRelInfo entries for them to guide the real planning.  (This is
-	 * rather inefficient; we could perhaps stop short of making a full Path
-	 * tree.  But this whole function is inefficient and slated for
-	 * destruction, so let's not contort query_planner for that.)
-	 */
-	{
-		PlannerInfo *subroot;
-
-		/*
-		 * Flat-copy the PlannerInfo to prevent modification of the original.
-		 */
-		subroot = makeNode(PlannerInfo);
-		memcpy(subroot, root, sizeof(PlannerInfo));
-
-		/*
-		 * Make a deep copy of the parsetree for this planning cycle to mess
-		 * around with, and change it to look like a SELECT.  (Hack alert: the
-		 * target RTE still has updatedCols set if this is an UPDATE, so that
-		 * expand_partitioned_rtentry will correctly update
-		 * subroot->partColsUpdated.)
-		 */
-		subroot->parse = copyObject(root->parse);
-
-		subroot->parse->commandType = CMD_SELECT;
-		subroot->parse->resultRelation = 0;
-
-		/*
-		 * Ensure the subroot has its own copy of the original
-		 * append_rel_list, since it'll be scribbled on.  (Note that at this
-		 * point, the list only contains AppendRelInfos for flattened UNION
-		 * ALL subqueries.)
-		 */
-		subroot->append_rel_list = copyObject(root->append_rel_list);
-
-		/*
-		 * Better make a private copy of the rowMarks, too.
-		 */
-		subroot->rowMarks = copyObject(root->rowMarks);
-
-		/* There shouldn't be any OJ info to translate, as yet */
-		Assert(subroot->join_info_list == NIL);
-		/* and we haven't created PlaceHolderInfos, either */
-		Assert(subroot->placeholder_list == NIL);
-
-		/* Generate Path(s) for accessing this result relation */
-		grouping_planner(subroot, true, 0.0 /* retrieve all tuples */ );
-
-		/* Extract the info we need. */
-		select_rtable = subroot->parse->rtable;
-		select_appinfos = subroot->append_rel_list;
-
-		/*
-		 * We need to propagate partColsUpdated back, too.  (The later
-		 * planning cycles will not set this because they won't run
-		 * expand_partitioned_rtentry for the UPDATE target.)
-		 */
-		root->partColsUpdated = subroot->partColsUpdated;
-	}
-
-	/*----------
-	 * Since only one rangetable can exist in the final plan, we need to make
-	 * sure that it contains all the RTEs needed for any child plan.  This is
-	 * complicated by the need to use separate subquery RTEs for each child.
-	 * We arrange the final rtable as follows:
-	 * 1. All original rtable entries (with their original RT indexes).
-	 * 2. All the relation RTEs generated for children of the target table.
-	 * 3. Subquery RTEs for children after the first.  We need N * (K - 1)
-	 *    RT slots for this, if there are N subqueries and K child tables.
-	 * 4. Additional RTEs generated during the child planning runs, such as
-	 *    children of inheritable RTEs other than the target table.
-	 * We assume that each child planning run will create an identical set
-	 * of type-4 RTEs.
-	 *
-	 * So the next thing to do is append the type-2 RTEs (the target table's
-	 * children) to the original rtable.  We look through select_appinfos
-	 * to find them.
-	 *
-	 * To identify which AppendRelInfos are relevant as we thumb through
-	 * select_appinfos, we need to look for both direct and indirect children
-	 * of top_parentRTindex, so we use a bitmap of known parent relids.
-	 * expand_inherited_rtentry() always processes a parent before any of that
-	 * parent's children, so we should see an intermediate parent before its
-	 * children.
-	 *----------
-	 */
-	child_appinfos = NIL;
-	old_child_rtis = NIL;
-	new_child_rtis = NIL;
-	parent_relids = bms_make_singleton(top_parentRTindex);
-	foreach(lc, select_appinfos)
-	{
-		AppendRelInfo *appinfo = lfirst_node(AppendRelInfo, lc);
-		RangeTblEntry *child_rte;
-
-		/* append_rel_list contains all append rels; ignore others */
-		if (!bms_is_member(appinfo->parent_relid, parent_relids))
-			continue;
-
-		/* remember relevant AppendRelInfos for use below */
-		child_appinfos = lappend(child_appinfos, appinfo);
-
-		/* extract RTE for this child rel */
-		child_rte = rt_fetch(appinfo->child_relid, select_rtable);
-
-		/* and append it to the original rtable */
-		parse->rtable = lappend(parse->rtable, child_rte);
-
-		/* remember child's index in the SELECT rtable */
-		old_child_rtis = lappend_int(old_child_rtis, appinfo->child_relid);
-
-		/* and its new index in the final rtable */
-		new_child_rtis = lappend_int(new_child_rtis, list_length(parse->rtable));
-
-		/* if child is itself partitioned, update parent_relids */
-		if (child_rte->inh)
-		{
-			Assert(child_rte->relkind == RELKIND_PARTITIONED_TABLE);
-			parent_relids = bms_add_member(parent_relids, appinfo->child_relid);
-		}
-	}
-
-	/*
-	 * It's possible that the RTIs we just assigned for the child rels in the
-	 * final rtable are different from what they were in the SELECT query.
-	 * Adjust the AppendRelInfos so that they will correctly map RT indexes to
-	 * the final indexes.  We can do this left-to-right since no child rel's
-	 * final RT index could be greater than what it had in the SELECT query.
-	 */
-	forboth(lc, old_child_rtis, lc2, new_child_rtis)
-	{
-		int			old_child_rti = lfirst_int(lc);
-		int			new_child_rti = lfirst_int(lc2);
-
-		if (old_child_rti == new_child_rti)
-			continue;			/* nothing to do */
-
-		Assert(old_child_rti > new_child_rti);
-
-		ChangeVarNodes((Node *) child_appinfos,
-					   old_child_rti, new_child_rti, 0);
-	}
-
-	/*
-	 * Now set up rangetable entries for subqueries for additional children
-	 * (the first child will just use the original ones).  These all have to
-	 * look more or less real, or EXPLAIN will get unhappy; so we just make
-	 * them all clones of the original subqueries.
-	 */
-	next_subquery_rti = list_length(parse->rtable) + 1;
-	if (subqueryRTindexes != NULL)
-	{
-		int			n_children = list_length(child_appinfos);
-
-		while (n_children-- > 1)
-		{
-			int			oldrti = -1;
-
-			while ((oldrti = bms_next_member(subqueryRTindexes, oldrti)) >= 0)
-			{
-				RangeTblEntry *subqrte;
-
-				subqrte = rt_fetch(oldrti, parse->rtable);
-				parse->rtable = lappend(parse->rtable, copyObject(subqrte));
-			}
-		}
-	}
-
-	/*
-	 * The query for each child is obtained by translating the query for its
-	 * immediate parent, since the AppendRelInfo data we have shows deltas
-	 * between parents and children.  We use the parent_parses array to
-	 * remember the appropriate query trees.  This is indexed by parent relid.
-	 * Since the maximum number of parents is limited by the number of RTEs in
-	 * the SELECT query, we use that number to allocate the array.  An extra
-	 * entry is needed since relids start from 1.
-	 */
-	parent_parses = (Query **) palloc0((list_length(select_rtable) + 1) *
-									   sizeof(Query *));
-	parent_parses[top_parentRTindex] = parse;
-
-	/*
-	 * And now we can get on with generating a plan for each child table.
-	 */
-	foreach(lc, child_appinfos)
-	{
-		AppendRelInfo *appinfo = lfirst_node(AppendRelInfo, lc);
-		Index		this_subquery_rti = next_subquery_rti;
-		Query	   *parent_parse;
-		PlannerInfo *subroot;
-		RangeTblEntry *child_rte;
-		RelOptInfo *sub_final_rel;
-		Path	   *subpath;
-
-		/*
-		 * expand_inherited_rtentry() always processes a parent before any of
-		 * that parent's children, so the parent query for this relation
-		 * should already be available.
-		 */
-		parent_parse = parent_parses[appinfo->parent_relid];
-		Assert(parent_parse != NULL);
-
-		/*
-		 * We need a working copy of the PlannerInfo so that we can control
-		 * propagation of information back to the main copy.
-		 */
-		subroot = makeNode(PlannerInfo);
-		memcpy(subroot, root, sizeof(PlannerInfo));
-
-		/*
-		 * Generate modified query with this rel as target.  We first apply
-		 * adjust_appendrel_attrs, which copies the Query and changes
-		 * references to the parent RTE to refer to the current child RTE,
-		 * then fool around with subquery RTEs.
-		 */
-		subroot->parse = (Query *)
-			adjust_appendrel_attrs(subroot,
-								   (Node *) parent_parse,
-								   1, &appinfo);
-
-		/*
-		 * If there are securityQuals attached to the parent, move them to the
-		 * child rel (they've already been transformed properly for that).
-		 */
-		parent_rte = rt_fetch(appinfo->parent_relid, subroot->parse->rtable);
-		child_rte = rt_fetch(appinfo->child_relid, subroot->parse->rtable);
-		child_rte->securityQuals = parent_rte->securityQuals;
-		parent_rte->securityQuals = NIL;
-
-		/*
-		 * HACK: setting this to a value other than INHKIND_NONE signals to
-		 * relation_excluded_by_constraints() to treat the result relation as
-		 * being an appendrel member.
-		 */
-		subroot->inhTargetKind =
-			(rootRelation != 0) ? INHKIND_PARTITIONED : INHKIND_INHERITED;
-
-		/*
-		 * If this child is further partitioned, remember it as a parent.
-		 * Since a partitioned table does not have any data, we don't need to
-		 * create a plan for it, and we can stop processing it here.  We do,
-		 * however, need to remember its modified PlannerInfo for use when
-		 * processing its children, since we'll update their varnos based on
-		 * the delta from immediate parent to child, not from top to child.
-		 *
-		 * Note: a very non-obvious point is that we have not yet added
-		 * duplicate subquery RTEs to the subroot's rtable.  We mustn't,
-		 * because then its children would have two sets of duplicates,
-		 * confusing matters.
-		 */
-		if (child_rte->inh)
-		{
-			Assert(child_rte->relkind == RELKIND_PARTITIONED_TABLE);
-			parent_parses[appinfo->child_relid] = subroot->parse;
-			continue;
-		}
-
-		/*
-		 * Set the nominal target relation of the ModifyTable node if not
-		 * already done.  If the target is a partitioned table, we already set
-		 * nominalRelation to refer to the partition root, above.  For
-		 * non-partitioned inheritance cases, we'll use the first child
-		 * relation (even if it's excluded) as the nominal target relation.
-		 * Because of the way expand_inherited_rtentry works, that should be
-		 * the RTE representing the parent table in its role as a simple
-		 * member of the inheritance set.
-		 *
-		 * It would be logically cleaner to *always* use the inheritance
-		 * parent RTE as the nominal relation; but that RTE is not otherwise
-		 * referenced in the plan in the non-partitioned inheritance case.
-		 * Instead the duplicate child RTE created by expand_inherited_rtentry
-		 * is used elsewhere in the plan, so using the original parent RTE
-		 * would give rise to confusing use of multiple aliases in EXPLAIN
-		 * output for what the user will think is the "same" table.  OTOH,
-		 * it's not a problem in the partitioned inheritance case, because
-		 * there is no duplicate RTE for the parent.
-		 */
-		if (nominalRelation < 0)
-			nominalRelation = appinfo->child_relid;
-
-		/*
-		 * As above, each child plan run needs its own append_rel_list and
-		 * rowmarks, which should start out as pristine copies of the
-		 * originals.  There can't be any references to UPDATE/DELETE target
-		 * rels in them; but there could be subquery references, which we'll
-		 * fix up in a moment.
-		 */
-		subroot->append_rel_list = copyObject(root->append_rel_list);
-		subroot->rowMarks = copyObject(root->rowMarks);
-
-		/*
-		 * If this isn't the first child Query, adjust Vars and jointree
-		 * entries to reference the appropriate set of subquery RTEs.
-		 */
-		if (final_rtable != NIL && subqueryRTindexes != NULL)
-		{
-			int			oldrti = -1;
-
-			while ((oldrti = bms_next_member(subqueryRTindexes, oldrti)) >= 0)
-			{
-				Index		newrti = next_subquery_rti++;
-
-				ChangeVarNodes((Node *) subroot->parse, oldrti, newrti, 0);
-				ChangeVarNodes((Node *) subroot->append_rel_list,
-							   oldrti, newrti, 0);
-				ChangeVarNodes((Node *) subroot->rowMarks, oldrti, newrti, 0);
-			}
-		}
-
-		/* There shouldn't be any OJ info to translate, as yet */
-		Assert(subroot->join_info_list == NIL);
-		/* and we haven't created PlaceHolderInfos, either */
-		Assert(subroot->placeholder_list == NIL);
-
-		/* Generate Path(s) for accessing this result relation */
-		grouping_planner(subroot, true, 0.0 /* retrieve all tuples */ );
-
-		/*
-		 * Select cheapest path in case there's more than one.  We always run
-		 * modification queries to conclusion, so we care only for the
-		 * cheapest-total path.
-		 */
-		sub_final_rel = fetch_upper_rel(subroot, UPPERREL_FINAL, NULL);
-		set_cheapest(sub_final_rel);
-		subpath = sub_final_rel->cheapest_total_path;
-
-		/*
-		 * If this child rel was excluded by constraint exclusion, exclude it
-		 * from the result plan.
-		 */
-		if (IS_DUMMY_REL(sub_final_rel))
-			continue;
-
-		/*
-		 * If this is the first non-excluded child, its post-planning rtable
-		 * becomes the initial contents of final_rtable; otherwise, copy its
-		 * modified subquery RTEs into final_rtable, to ensure we have sane
-		 * copies of those.  Also save the first non-excluded child's version
-		 * of the rowmarks list; we assume all children will end up with
-		 * equivalent versions of that.  Likewise for append_rel_list.
-		 */
-		if (final_rtable == NIL)
-		{
-			final_rtable = subroot->parse->rtable;
-			final_rowmarks = subroot->rowMarks;
-			final_appendrels = subroot->append_rel_list;
-		}
-		else
-		{
-			Assert(list_length(final_rtable) ==
-				   list_length(subroot->parse->rtable));
-			if (subqueryRTindexes != NULL)
-			{
-				int			oldrti = -1;
-
-				while ((oldrti = bms_next_member(subqueryRTindexes, oldrti)) >= 0)
-				{
-					Index		newrti = this_subquery_rti++;
-					RangeTblEntry *subqrte;
-					ListCell   *newrticell;
-
-					subqrte = rt_fetch(newrti, subroot->parse->rtable);
-					newrticell = list_nth_cell(final_rtable, newrti - 1);
-					lfirst(newrticell) = subqrte;
-				}
-			}
-		}
-
-		/*
-		 * We need to collect all the RelOptInfos from all child plans into
-		 * the main PlannerInfo, since setrefs.c will need them.  We use the
-		 * last child's simple_rel_array, so we have to propagate forward the
-		 * RelOptInfos that were already built in previous children.
-		 */
-		Assert(subroot->simple_rel_array_size >= save_rel_array_size);
-		for (rti = 1; rti < save_rel_array_size; rti++)
-		{
-			RelOptInfo *brel = save_rel_array[rti];
-
-			if (brel)
-				subroot->simple_rel_array[rti] = brel;
-		}
-		save_rel_array_size = subroot->simple_rel_array_size;
-		save_rel_array = subroot->simple_rel_array;
-		save_append_rel_array = subroot->append_rel_array;
-
-		/*
-		 * Make sure any initplans from this rel get into the outer list. Note
-		 * we're effectively assuming all children generate the same
-		 * init_plans.
-		 */
-		root->init_plans = subroot->init_plans;
-
-		/* Build list of sub-paths */
-		subpaths = lappend(subpaths, subpath);
-
-		/* Build list of modified subroots, too */
-		subroots = lappend(subroots, subroot);
-
-		/* Build list of target-relation RT indexes */
-		resultRelations = lappend_int(resultRelations, appinfo->child_relid);
-
-		/* Accumulate lists of UPDATE target columns */
-		if (parse->commandType == CMD_UPDATE)
-			updateColnosLists = lappend(updateColnosLists,
-										subroot->update_colnos);
-
-		/* Build lists of per-relation WCO and RETURNING targetlists */
-		if (parse->withCheckOptions)
-			withCheckOptionLists = lappend(withCheckOptionLists,
-										   subroot->parse->withCheckOptions);
-		if (parse->returningList)
-			returningLists = lappend(returningLists,
-									 subroot->parse->returningList);
-
-		Assert(!parse->onConflict);
-	}
-
-	/* Result path must go into outer query's FINAL upperrel */
-	final_rel = fetch_upper_rel(root, UPPERREL_FINAL, NULL);
-
-	/*
-	 * We don't currently worry about setting final_rel's consider_parallel
-	 * flag in this case, nor about allowing FDWs or create_upper_paths_hook
-	 * to get control here.
-	 */
-
-	if (subpaths == NIL)
-	{
-		/*
-		 * We managed to exclude every child rel, so generate a dummy path
-		 * representing the empty set.  Although it's clear that no data will
-		 * be updated or deleted, we will still need to have a ModifyTable
-		 * node so that any statement triggers are executed.  (This could be
-		 * cleaner if we fixed nodeModifyTable.c to support zero child nodes,
-		 * but that probably wouldn't be a net win.)
-		 */
-		Path	   *dummy_path;
-
-		/* tlist processing never got done, either */
-		root->processed_tlist = preprocess_targetlist(root);
-		final_rel->reltarget = create_pathtarget(root, root->processed_tlist);
-
-		/* Make a dummy path, cf set_dummy_rel_pathlist() */
-		dummy_path = (Path *) create_append_path(NULL, final_rel, NIL, NIL,
-												 NIL, NULL, 0, false,
-												 -1);
-
-		/* These lists must be nonempty to make a valid ModifyTable node */
-		subpaths = list_make1(dummy_path);
-		subroots = list_make1(root);
-		resultRelations = list_make1_int(parse->resultRelation);
-		if (parse->commandType == CMD_UPDATE)
-			updateColnosLists = lappend(updateColnosLists,
-										root->update_colnos);
-		if (parse->withCheckOptions)
-			withCheckOptionLists = list_make1(parse->withCheckOptions);
-		if (parse->returningList)
-			returningLists = list_make1(parse->returningList);
-		/* Disable tuple routing, too, just to be safe */
-		root->partColsUpdated = false;
-	}
-	else
-	{
-		/*
-		 * Put back the final adjusted rtable into the original copy of the
-		 * Query.  (We mustn't do this if we found no non-excluded children,
-		 * since we never saved an adjusted rtable at all.)
-		 */
-		parse->rtable = final_rtable;
-		root->simple_rel_array_size = save_rel_array_size;
-		root->simple_rel_array = save_rel_array;
-		root->append_rel_array = save_append_rel_array;
-
-		/* Must reconstruct original's simple_rte_array, too */
-		root->simple_rte_array = (RangeTblEntry **)
-			palloc0((list_length(final_rtable) + 1) * sizeof(RangeTblEntry *));
-		rti = 1;
-		foreach(lc, final_rtable)
-		{
-			RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
-
-			root->simple_rte_array[rti++] = rte;
-		}
-
-		/* Put back adjusted rowmarks and appendrels, too */
-		root->rowMarks = final_rowmarks;
-		root->append_rel_list = final_appendrels;
-	}
-
-	/*
-	 * If there was a FOR [KEY] UPDATE/SHARE clause, the LockRows node will
-	 * have dealt with fetching non-locked marked rows, else we need to have
-	 * ModifyTable do that.
-	 */
-	if (parse->rowMarks)
-		rowMarks = NIL;
-	else
-		rowMarks = root->rowMarks;
-
-	/* Create Path representing a ModifyTable to do the UPDATE/DELETE work */
-	add_path(final_rel, (Path *)
-			 create_modifytable_path(root, final_rel,
-									 parse->commandType,
-									 parse->canSetTag,
-									 nominalRelation,
-									 rootRelation,
-									 root->partColsUpdated,
-									 resultRelations,
-									 subpaths,
-									 subroots,
-									 updateColnosLists,
-									 withCheckOptionLists,
-									 returningLists,
-									 rowMarks,
-									 NULL,
-									 assign_special_exec_param(root)));
-}
-
 /*--------------------
  * grouping_planner
  *	  Perform planning steps related to grouping, aggregation, etc.
@@ -1813,11 +1197,6 @@ inheritance_planner(PlannerInfo *root)
  * This function adds all required top-level processing to the scan/join
  * Path(s) produced by query_planner.
  *
- * If inheritance_update is true, we're being called from inheritance_planner
- * and should not include a ModifyTable step in the resulting Path(s).
- * (inheritance_planner will create a single ModifyTable node covering all the
- * target tables.)
- *
  * tuple_fraction is the fraction of tuples we expect will be retrieved.
  * tuple_fraction is interpreted as follows:
  *	  0: expect all tuples to be retrieved (normal case)
@@ -1835,8 +1214,7 @@ inheritance_planner(PlannerInfo *root)
  *--------------------
  */
 static void
-grouping_planner(PlannerInfo *root, bool inheritance_update,
-				 double tuple_fraction)
+grouping_planner(PlannerInfo *root, double tuple_fraction)
 {
 	Query	   *parse = root->parse;
 	int64		offset_est = 0;
@@ -1980,7 +1358,7 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 		 * that we can transfer its decoration (resnames etc) to the topmost
 		 * tlist of the finished Plan.  This is kept in processed_tlist.
 		 */
-		root->processed_tlist = preprocess_targetlist(root);
+		preprocess_targetlist(root);
 
 		/*
 		 * Mark all the aggregates with resolved aggtranstypes, and detect
@@ -2318,17 +1696,117 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 		}
 
 		/*
-		 * If this is an INSERT/UPDATE/DELETE, and we're not being called from
-		 * inheritance_planner, add the ModifyTable node.
+		 * If this is an INSERT/UPDATE/DELETE, add the ModifyTable node.
 		 */
-		if (parse->commandType != CMD_SELECT && !inheritance_update)
+		if (parse->commandType != CMD_SELECT)
 		{
 			Index		rootRelation;
-			List *updateColnosLists;
-			List	   *withCheckOptionLists;
-			List	   *returningLists;
+			List	   *resultRelations = NIL;
+			List	   *updateColnosLists = NIL;
+			List	   *withCheckOptionLists = NIL;
+			List	   *returningLists = NIL;
 			List	   *rowMarks;
 
+			if (bms_membership(root->all_result_relids) == BMS_MULTIPLE)
+			{
+				/* Inherited UPDATE/DELETE */
+				RelOptInfo *top_result_rel = find_base_rel(root,
+														   parse->resultRelation);
+				int			resultRelation = -1;
+
+				/* Add only leaf children to ModifyTable. */
+				while ((resultRelation = bms_next_member(root->leaf_result_relids,
+														 resultRelation)) >= 0)
+				{
+					RelOptInfo *this_result_rel = find_base_rel(root,
+																resultRelation);
+
+					/*
+					 * Also exclude any leaf rels that have turned dummy since
+					 * being added to the list, for example, by being excluded
+					 * by constraint exclusion.
+					 */
+					if (IS_DUMMY_REL(this_result_rel))
+						continue;
+
+					/* Build per-target-rel lists needed by ModifyTable */
+					resultRelations = lappend_int(resultRelations,
+												  resultRelation);
+					if (parse->commandType == CMD_UPDATE)
+					{
+						List	   *update_colnos = root->update_colnos;
+
+						if (this_result_rel != top_result_rel)
+							update_colnos =
+								adjust_inherited_attnums_multilevel(root,
+																	update_colnos,
+																	this_result_rel->relid,
+																	top_result_rel->relid);
+						updateColnosLists = lappend(updateColnosLists,
+													update_colnos);
+					}
+					if (parse->withCheckOptions)
+					{
+						List	   *withCheckOptions = parse->withCheckOptions;
+
+						if (this_result_rel != top_result_rel)
+							withCheckOptions = (List *)
+								adjust_appendrel_attrs_multilevel(root,
+																  (Node *) withCheckOptions,
+																  this_result_rel->relids,
+																  top_result_rel->relids);
+						withCheckOptionLists = lappend(withCheckOptionLists,
+													   withCheckOptions);
+					}
+					if (parse->returningList)
+					{
+						List	   *returningList = parse->returningList;
+
+						if (this_result_rel != top_result_rel)
+							returningList = (List *)
+								adjust_appendrel_attrs_multilevel(root,
+																  (Node *) returningList,
+																  this_result_rel->relids,
+																  top_result_rel->relids);
+						returningLists = lappend(returningLists,
+												 returningList);
+					}
+				}
+
+				if (resultRelations == NIL)
+				{
+					/*
+					 * We managed to exclude every child rel, so generate a
+					 * dummy one-relation plan using info for the top target
+					 * rel (even though that may not be a leaf target).
+					 * Although it's clear that no data will be updated or
+					 * deleted, we still need to have a ModifyTable node so
+					 * that any statement triggers will be executed.  (This
+					 * could be cleaner if we fixed nodeModifyTable.c to allow
+					 * zero target relations, but that probably wouldn't be a
+					 * net win.)
+					 */
+					resultRelations = list_make1_int(parse->resultRelation);
+					if (parse->commandType == CMD_UPDATE)
+						updateColnosLists = list_make1(root->update_colnos);
+					if (parse->withCheckOptions)
+						withCheckOptionLists = list_make1(parse->withCheckOptions);
+					if (parse->returningList)
+						returningLists = list_make1(parse->returningList);
+				}
+			}
+			else
+			{
+				/* Single-relation INSERT/UPDATE/DELETE. */
+				resultRelations = list_make1_int(parse->resultRelation);
+				if (parse->commandType == CMD_UPDATE)
+					updateColnosLists = list_make1(root->update_colnos);
+				if (parse->withCheckOptions)
+					withCheckOptionLists = list_make1(parse->withCheckOptions);
+				if (parse->returningList)
+					returningLists = list_make1(parse->returningList);
+			}
+
 			/*
 			 * If target is a partition root table, we need to mark the
 			 * ModifyTable node appropriately for that.
@@ -2339,26 +1817,6 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 			else
 				rootRelation = 0;
 
-			/* Set up the UPDATE target columns list-of-lists, if needed. */
-			if (parse->commandType == CMD_UPDATE)
-				updateColnosLists = list_make1(root->update_colnos);
-			else
-				updateColnosLists = NIL;
-
-			/*
-			 * Set up the WITH CHECK OPTION and RETURNING lists-of-lists, if
-			 * needed.
-			 */
-			if (parse->withCheckOptions)
-				withCheckOptionLists = list_make1(parse->withCheckOptions);
-			else
-				withCheckOptionLists = NIL;
-
-			if (parse->returningList)
-				returningLists = list_make1(parse->returningList);
-			else
-				returningLists = NIL;
-
 			/*
 			 * If there was a FOR [KEY] UPDATE/SHARE clause, the LockRows node
 			 * will have dealt with fetching non-locked marked rows, else we
@@ -2371,14 +1829,13 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 
 			path = (Path *)
 				create_modifytable_path(root, final_rel,
+										path,
 										parse->commandType,
 										parse->canSetTag,
 										parse->resultRelation,
 										rootRelation,
-										false,
-										list_make1_int(parse->resultRelation),
-										list_make1(path),
-										list_make1(root),
+										root->partColsUpdated,
+										resultRelations,
 										updateColnosLists,
 										withCheckOptionLists,
 										returningLists,
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 42f088ad71..578ce4e6c8 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -867,6 +867,29 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 					set_upper_references(root, plan, rtoffset);
 				else
 				{
+					/*
+					 * The tlist of a childless Result could contain
+					 * unresolved ROWID_VAR Vars, in case it's representing a
+					 * target relation which is completely empty because of
+					 * constraint exclusion.  Replace any such Vars by null
+					 * constants, as though they'd been resolved for a leaf
+					 * scan node that doesn't support them.  We could have
+					 * fix_scan_expr do this, but since the case is only
+					 * expected to occur here, it seems safer to special-case
+					 * it here and keep the assertions that ROWID_VARs
+					 * shouldn't be seen by fix_scan_expr.
+					 */
+					foreach(l, splan->plan.targetlist)
+					{
+						TargetEntry *tle = (TargetEntry *) lfirst(l);
+						Var		   *var = (Var *) tle->expr;
+
+						if (var && IsA(var, Var) && var->varno == ROWID_VAR)
+							tle->expr = (Expr *) makeNullConst(var->vartype,
+															   var->vartypmod,
+															   var->varcollid);
+					}
+
 					splan->plan.targetlist =
 						fix_scan_list(root, splan->plan.targetlist,
 									  rtoffset, NUM_EXEC_TLIST(plan));
@@ -896,23 +919,20 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				if (splan->returningLists)
 				{
 					List	   *newRL = NIL;
+					Plan	   *subplan = outerPlan(splan);
 					ListCell   *lcrl,
-							   *lcrr,
-							   *lcp;
+							   *lcrr;
 
 					/*
-					 * Pass each per-subplan returningList through
+					 * Pass each per-resultrel returningList through
 					 * set_returning_clause_references().
 					 */
 					Assert(list_length(splan->returningLists) == list_length(splan->resultRelations));
-					Assert(list_length(splan->returningLists) == list_length(splan->plans));
-					forthree(lcrl, splan->returningLists,
-							 lcrr, splan->resultRelations,
-							 lcp, splan->plans)
+					forboth(lcrl, splan->returningLists,
+							lcrr, splan->resultRelations)
 					{
 						List	   *rlist = (List *) lfirst(lcrl);
 						Index		resultrel = lfirst_int(lcrr);
-						Plan	   *subplan = (Plan *) lfirst(lcp);
 
 						rlist = set_returning_clause_references(root,
 																rlist,
@@ -982,12 +1002,6 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 					rc->rti += rtoffset;
 					rc->prti += rtoffset;
 				}
-				foreach(l, splan->plans)
-				{
-					lfirst(l) = set_plan_refs(root,
-											  (Plan *) lfirst(l),
-											  rtoffset);
-				}
 
 				/*
 				 * Append this ModifyTable node's final result relation RT
@@ -1791,6 +1805,13 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan,
  * choosing the best implementation for AlternativeSubPlans,
  * looking up operator opcode info for OpExpr and related nodes,
  * and adding OIDs from regclass Const nodes into root->glob->relationOids.
+ *
+ * 'node': the expression to be modified
+ * 'rtoffset': how much to increment varnos by
+ * 'num_exec': estimated number of executions of expression
+ *
+ * The expression tree is either copied-and-modified, or modified in-place
+ * if that seems safe.
  */
 static Node *
 fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec)
@@ -1839,11 +1860,12 @@ fix_scan_expr_mutator(Node *node, fix_scan_expr_context *context)
 		Assert(var->varlevelsup == 0);
 
 		/*
-		 * We should not see any Vars marked INNER_VAR or OUTER_VAR.  But an
-		 * indexqual expression could contain INDEX_VAR Vars.
+		 * We should not see Vars marked INNER_VAR, OUTER_VAR, or ROWID_VAR.
+		 * But an indexqual expression could contain INDEX_VAR Vars.
 		 */
 		Assert(var->varno != INNER_VAR);
 		Assert(var->varno != OUTER_VAR);
+		Assert(var->varno != ROWID_VAR);
 		if (!IS_SPECIAL_VARNO(var->varno))
 			var->varno += context->rtoffset;
 		if (var->varnosyn > 0)
@@ -1906,6 +1928,7 @@ fix_scan_expr_walker(Node *node, fix_scan_expr_context *context)
 {
 	if (node == NULL)
 		return false;
+	Assert(!(IsA(node, Var) && ((Var *) node)->varno == ROWID_VAR));
 	Assert(!IsA(node, PlaceHolderVar));
 	Assert(!IsA(node, AlternativeSubPlan));
 	fix_expr_common(context->root, node);
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index f3e46e0959..b12ab7de2d 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2533,7 +2533,6 @@ finalize_plan(PlannerInfo *root, Plan *plan,
 		case T_ModifyTable:
 			{
 				ModifyTable *mtplan = (ModifyTable *) plan;
-				ListCell   *l;
 
 				/* Force descendant scan nodes to reference epqParam */
 				locally_added_param = mtplan->epqParam;
@@ -2548,16 +2547,6 @@ finalize_plan(PlannerInfo *root, Plan *plan,
 				finalize_primnode((Node *) mtplan->onConflictWhere,
 								  &context);
 				/* exclRelTlist contains only Vars, doesn't need examination */
-				foreach(l, mtplan->plans)
-				{
-					context.paramids =
-						bms_add_members(context.paramids,
-										finalize_plan(root,
-													  (Plan *) lfirst(l),
-													  gather_param,
-													  valid_params,
-													  scan_params));
-				}
 			}
 			break;
 
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index e18553ac7c..62a1668796 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -920,7 +920,10 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	subroot->multiexpr_params = NIL;
 	subroot->eq_classes = NIL;
 	subroot->ec_merging_done = false;
+	subroot->all_result_relids = NULL;
+	subroot->leaf_result_relids = NULL;
 	subroot->append_rel_list = NIL;
+	subroot->row_identity_vars = NIL;
 	subroot->rowMarks = NIL;
 	memset(subroot->upper_rels, 0, sizeof(subroot->upper_rels));
 	memset(subroot->upper_targets, 0, sizeof(subroot->upper_targets));
@@ -929,7 +932,6 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	subroot->grouping_map = NULL;
 	subroot->minmax_aggs = NIL;
 	subroot->qual_security_level = 0;
-	subroot->inhTargetKind = INHKIND_NONE;
 	subroot->hasRecursion = false;
 	subroot->wt_param_id = -1;
 	subroot->non_recursive_path = NULL;
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index 488e8cfd4d..363132185d 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -11,7 +11,8 @@
  *
  * For UPDATE and DELETE queries, the targetlist must also contain "junk"
  * tlist entries needed to allow the executor to identify the rows to be
- * updated or deleted; for example, the ctid of a heap row.
+ * updated or deleted; for example, the ctid of a heap row.  (The planner
+ * adds these; they're not in what we receive from the planner/rewriter.)
  *
  * For all query types, there can be additional junk tlist entries, such as
  * sort keys, Vars needed for a RETURNING list, and row ID information needed
@@ -19,20 +20,9 @@
  *
  * The query rewrite phase also does preprocessing of the targetlist (see
  * rewriteTargetListIU).  The division of labor between here and there is
- * partially historical, but it's not entirely arbitrary.  In particular,
- * consider an UPDATE across an inheritance tree.  What rewriteTargetListIU
- * does need be done only once (because it depends only on the properties of
- * the parent relation).  What's done here has to be done over again for each
- * child relation, because it depends on the properties of the child, which
- * might be of a different relation type, or have more columns and/or a
- * different column order than the parent.
- *
- * The fact that rewriteTargetListIU sorts non-resjunk tlist entries by column
- * position, which expand_targetlist depends on, violates the above comment
- * because the sorting is only valid for the parent relation.  In inherited
- * UPDATE cases, adjust_inherited_tlist runs in between to take care of fixing
- * the tlists for child tables to keep expand_targetlist happy.  We do it like
- * that because it's faster in typical non-inherited cases.
+ * partially historical, but it's not entirely arbitrary.  The stuff done
+ * here is closely connected to physical access to tables, whereas the
+ * rewriter's work is more concerned with SQL semantics.
  *
  *
  * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
@@ -46,19 +36,17 @@
 
 #include "postgres.h"
 
-#include "access/sysattr.h"
 #include "access/table.h"
-#include "catalog/pg_type.h"
 #include "nodes/makefuncs.h"
+#include "optimizer/appendinfo.h"
 #include "optimizer/optimizer.h"
 #include "optimizer/prep.h"
 #include "optimizer/tlist.h"
 #include "parser/parse_coerce.h"
 #include "parser/parsetree.h"
-#include "rewrite/rewriteHandler.h"
 #include "utils/rel.h"
 
-static List *make_update_colnos(List *tlist);
+static List *extract_update_colnos(List *tlist);
 static List *expand_targetlist(List *tlist, int command_type,
 							   Index result_relation, Relation rel);
 
@@ -67,13 +55,15 @@ static List *expand_targetlist(List *tlist, int command_type,
  * preprocess_targetlist
  *	  Driver for preprocessing the parse tree targetlist.
  *
- *	  Returns the new targetlist.
+ * The preprocessed targetlist is returned in root->processed_tlist.
+ * Also, if this is an UPDATE, we return a list of target column numbers
+ * in root->update_colnos.  (Resnos in processed_tlist will be consecutive,
+ * so do not look at that to find out which columns are targets!)
  *
  * As a side effect, if there's an ON CONFLICT UPDATE clause, its targetlist
- * is also preprocessed (and updated in-place).  Also, if this is an UPDATE,
- * we return a list of target column numbers in root->update_colnos.
+ * is also preprocessed (and updated in-place).
  */
-List *
+void
 preprocess_targetlist(PlannerInfo *root)
 {
 	Query	   *parse = root->parse;
@@ -106,29 +96,39 @@ preprocess_targetlist(PlannerInfo *root)
 	else
 		Assert(command_type == CMD_SELECT);
 
-	/*
-	 * For UPDATE/DELETE, add any junk column(s) needed to allow the executor
-	 * to identify the rows to be updated or deleted.  Note that this step
-	 * scribbles on parse->targetList, which is not very desirable, but we
-	 * keep it that way to avoid changing APIs used by FDWs.
-	 */
-	if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
-		rewriteTargetListUD(parse, target_rte, target_relation);
-
 	/*
 	 * In an INSERT, the executor expects the targetlist to match the exact
 	 * order of the target table's attributes, including entries for
 	 * attributes not mentioned in the source query.
 	 *
 	 * In an UPDATE, we don't rearrange the tlist order, but we need to make a
-	 * separate list of the target attribute numbers, in tlist order.
+	 * separate list of the target attribute numbers, in tlist order, and then
+	 * renumber the processed_tlist entries to be consecutive.
 	 */
 	tlist = parse->targetList;
 	if (command_type == CMD_INSERT)
 		tlist = expand_targetlist(tlist, command_type,
 								  result_relation, target_relation);
 	else if (command_type == CMD_UPDATE)
-		root->update_colnos = make_update_colnos(tlist);
+		root->update_colnos = extract_update_colnos(tlist);
+
+	/*
+	 * For non-inherited UPDATE/DELETE, register any junk column(s) needed to
+	 * allow the executor to identify the rows to be updated or deleted.  In
+	 * the inheritance case, we do nothing now, leaving this to be dealt with
+	 * when expand_inherited_rtentry() makes the leaf target relations.  (But
+	 * there might not be any leaf target relations, in which case we must do
+	 * this in distribute_row_identity_vars().)
+	 */
+	if ((command_type == CMD_UPDATE || command_type == CMD_DELETE) &&
+		!target_rte->inh)
+	{
+		/* row-identity logic expects to add stuff to processed_tlist */
+		root->processed_tlist = tlist;
+		add_row_identity_columns(root, result_relation,
+								 target_rte, target_relation);
+		tlist = root->processed_tlist;
+	}
 
 	/*
 	 * Add necessary junk columns for rowmarked rels.  These values are needed
@@ -136,6 +136,14 @@ preprocess_targetlist(PlannerInfo *root)
 	 * rechecking.  See comments for PlanRowMark in plannodes.h.  If you
 	 * change this stanza, see also expand_inherited_rtentry(), which has to
 	 * be able to add on junk columns equivalent to these.
+	 *
+	 * (Someday it might be useful to fold these resjunk columns into the
+	 * row-identity-column management used for UPDATE/DELETE.  Today is not
+	 * that day, however.  One notable issue is that it seems important that
+	 * the whole-row Vars made here use the real table rowtype, not RECORD, so
+	 * that conversion to/from child relations' rowtypes will happen.  Also,
+	 * since these entries don't potentially bloat with more and more child
+	 * relations, there's not really much need for column sharing.)
 	 */
 	foreach(lc, root->rowMarks)
 	{
@@ -235,6 +243,8 @@ preprocess_targetlist(PlannerInfo *root)
 		list_free(vars);
 	}
 
+	root->processed_tlist = tlist;
+
 	/*
 	 * If there's an ON CONFLICT UPDATE clause, preprocess its targetlist too
 	 * while we have the relation open.
@@ -248,22 +258,25 @@ preprocess_targetlist(PlannerInfo *root)
 
 	if (target_relation)
 		table_close(target_relation, NoLock);
-
-	return tlist;
 }
 
 /*
- * make_update_colnos
+ * extract_update_colnos
  * 		Extract a list of the target-table column numbers that
- * 		an UPDATE's targetlist wants to assign to.
+ * 		an UPDATE's targetlist wants to assign to, then renumber.
  *
- * We just need to capture the resno's of the non-junk tlist entries.
+ * The convention in the parser and rewriter is that the resnos in an
+ * UPDATE's non-resjunk TLE entries are the target column numbers
+ * to assign to.  Here, we extract that info into a separate list, and
+ * then convert the tlist to the sequential-numbering convention that's
+ * used by all other query types.
  */
 static List *
-make_update_colnos(List *tlist)
+extract_update_colnos(List *tlist)
 {
-	List*update_colnos = NIL;
-	ListCell *lc;
+	List	   *update_colnos = NIL;
+	AttrNumber	nextresno = 1;
+	ListCell   *lc;
 
 	foreach(lc, tlist)
 	{
@@ -271,6 +284,7 @@ make_update_colnos(List *tlist)
 
 		if (!tle->resjunk)
 			update_colnos = lappend_int(update_colnos, tle->resno);
+		tle->resno = nextresno++;
 	}
 	return update_colnos;
 }
diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c
index 86922a273c..af46f581ac 100644
--- a/src/backend/optimizer/util/appendinfo.c
+++ b/src/backend/optimizer/util/appendinfo.c
@@ -15,9 +15,12 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/table.h"
+#include "foreign/fdwapi.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/appendinfo.h"
+#include "optimizer/pathnode.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
@@ -37,8 +40,6 @@ static void make_inh_translation_list(Relation oldrelation,
 									  AppendRelInfo *appinfo);
 static Node *adjust_appendrel_attrs_mutator(Node *node,
 											adjust_appendrel_attrs_context *context);
-static List *adjust_inherited_tlist(List *tlist,
-									AppendRelInfo *context);
 
 
 /*
@@ -194,7 +195,6 @@ Node *
 adjust_appendrel_attrs(PlannerInfo *root, Node *node, int nappinfos,
 					   AppendRelInfo **appinfos)
 {
-	Node	   *result;
 	adjust_appendrel_attrs_context context;
 
 	context.root = root;
@@ -204,40 +204,10 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, int nappinfos,
 	/* If there's nothing to adjust, don't call this function. */
 	Assert(nappinfos >= 1 && appinfos != NULL);
 
-	/*
-	 * Must be prepared to start with a Query or a bare expression tree.
-	 */
-	if (node && IsA(node, Query))
-	{
-		Query	   *newnode;
-		int			cnt;
+	/* Should never be translating a Query tree. */
+	Assert(node == NULL || !IsA(node, Query));
 
-		newnode = query_tree_mutator((Query *) node,
-									 adjust_appendrel_attrs_mutator,
-									 (void *) &context,
-									 QTW_IGNORE_RC_SUBQUERIES);
-		for (cnt = 0; cnt < nappinfos; cnt++)
-		{
-			AppendRelInfo *appinfo = appinfos[cnt];
-
-			if (newnode->resultRelation == appinfo->parent_relid)
-			{
-				newnode->resultRelation = appinfo->child_relid;
-				/* Fix tlist resnos too, if it's inherited UPDATE */
-				if (newnode->commandType == CMD_UPDATE)
-					newnode->targetList =
-						adjust_inherited_tlist(newnode->targetList,
-											   appinfo);
-				break;
-			}
-		}
-
-		result = (Node *) newnode;
-	}
-	else
-		result = adjust_appendrel_attrs_mutator(node, &context);
-
-	return result;
+	return adjust_appendrel_attrs_mutator(node, &context);
 }
 
 static Node *
@@ -343,6 +313,57 @@ adjust_appendrel_attrs_mutator(Node *node,
 			}
 			/* system attributes don't need any other translation */
 		}
+		else if (var->varno == ROWID_VAR)
+		{
+			/*
+			 * If it's a ROWID_VAR placeholder, see if we've reached a leaf
+			 * target rel, for which we can translate the Var to a specific
+			 * instantiation.  We should never be asked to translate to a set
+			 * of relids containing more than one leaf target rel, so the
+			 * answer will be unique.  If we're still considering non-leaf
+			 * inheritance levels, return the ROWID_VAR Var as-is.
+			 */
+			Relids		leaf_result_relids = context->root->leaf_result_relids;
+			Index		leaf_relid = 0;
+
+			for (cnt = 0; cnt < nappinfos; cnt++)
+			{
+				if (bms_is_member(appinfos[cnt]->child_relid,
+								  leaf_result_relids))
+				{
+					if (leaf_relid)
+						elog(ERROR, "cannot translate to multiple leaf relids");
+					leaf_relid = appinfos[cnt]->child_relid;
+				}
+			}
+
+			if (leaf_relid)
+			{
+				RowIdentityVarInfo *ridinfo = (RowIdentityVarInfo *)
+				list_nth(context->root->row_identity_vars, var->varattno - 1);
+
+				if (bms_is_member(leaf_relid, ridinfo->rowidrels))
+				{
+					/* Substitute the Var given in the RowIdentityVarInfo */
+					var = copyObject(ridinfo->rowidvar);
+					/* ... but use the correct relid */
+					var->varno = leaf_relid;
+					/* varnosyn in the RowIdentityVarInfo is probably wrong */
+					var->varnosyn = 0;
+					var->varattnosyn = 0;
+				}
+				else
+				{
+					/*
+					 * This leaf rel can't return the desired value, so
+					 * substitute a NULL of the correct type.
+					 */
+					return (Node *) makeNullConst(var->vartype,
+												  var->vartypmod,
+												  var->varcollid);
+				}
+			}
+		}
 		return (Node *) var;
 	}
 	if (IsA(node, CurrentOfExpr))
@@ -361,44 +382,6 @@ adjust_appendrel_attrs_mutator(Node *node,
 		}
 		return (Node *) cexpr;
 	}
-	if (IsA(node, RangeTblRef))
-	{
-		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
-
-		for (cnt = 0; cnt < nappinfos; cnt++)
-		{
-			AppendRelInfo *appinfo = appinfos[cnt];
-
-			if (rtr->rtindex == appinfo->parent_relid)
-			{
-				rtr->rtindex = appinfo->child_relid;
-				break;
-			}
-		}
-		return (Node *) rtr;
-	}
-	if (IsA(node, JoinExpr))
-	{
-		/* Copy the JoinExpr node with correct mutation of subnodes */
-		JoinExpr   *j;
-		AppendRelInfo *appinfo;
-
-		j = (JoinExpr *) expression_tree_mutator(node,
-												 adjust_appendrel_attrs_mutator,
-												 (void *) context);
-		/* now fix JoinExpr's rtindex (probably never happens) */
-		for (cnt = 0; cnt < nappinfos; cnt++)
-		{
-			appinfo = appinfos[cnt];
-
-			if (j->rtindex == appinfo->parent_relid)
-			{
-				j->rtindex = appinfo->child_relid;
-				break;
-			}
-		}
-		return (Node *) j;
-	}
 	if (IsA(node, PlaceHolderVar))
 	{
 		/* Copy the PlaceHolderVar node with correct mutation of subnodes */
@@ -486,6 +469,9 @@ adjust_appendrel_attrs_mutator(Node *node,
 	 */
 	Assert(!IsA(node, SubLink));
 	Assert(!IsA(node, Query));
+	/* We should never see these Query substructures, either. */
+	Assert(!IsA(node, RangeTblRef));
+	Assert(!IsA(node, JoinExpr));
 
 	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
 								   (void *) context);
@@ -621,100 +607,101 @@ adjust_child_relids_multilevel(PlannerInfo *root, Relids relids,
 }
 
 /*
- * Adjust the targetlist entries of an inherited UPDATE operation
- *
- * The expressions have already been fixed, but we have to make sure that
- * the target resnos match the child table (they may not, in the case of
- * a column that was added after-the-fact by ALTER TABLE).  In some cases
- * this can force us to re-order the tlist to preserve resno ordering.
- * (We do all this work in special cases so that preptlist.c is fast for
- * the typical case.)
- *
- * The given tlist has already been through expression_tree_mutator;
- * therefore the TargetEntry nodes are fresh copies that it's okay to
- * scribble on.
- *
- * Note that this is not needed for INSERT because INSERT isn't inheritable.
+ * adjust_inherited_attnums
+ *	  Translate an integer list of attribute numbers from parent to child.
  */
-static List *
-adjust_inherited_tlist(List *tlist, AppendRelInfo *context)
+List *
+adjust_inherited_attnums(List *attnums, AppendRelInfo *context)
 {
-	bool		changed_it = false;
-	ListCell   *tl;
-	List	   *new_tlist;
-	bool		more;
-	int			attrno;
+	List	   *result = NIL;
+	ListCell   *lc;
 
 	/* This should only happen for an inheritance case, not UNION ALL */
 	Assert(OidIsValid(context->parent_reloid));
 
-	/* Scan tlist and update resnos to match attnums of child rel */
-	foreach(tl, tlist)
+	/* Look up each attribute in the AppendRelInfo's translated_vars list */
+	foreach(lc, attnums)
 	{
-		TargetEntry *tle = (TargetEntry *) lfirst(tl);
+		AttrNumber	parentattno = lfirst_int(lc);
 		Var		   *childvar;
 
-		if (tle->resjunk)
-			continue;			/* ignore junk items */
-
 		/* Look up the translation of this column: it must be a Var */
-		if (tle->resno <= 0 ||
-			tle->resno > list_length(context->translated_vars))
+		if (parentattno <= 0 ||
+			parentattno > list_length(context->translated_vars))
 			elog(ERROR, "attribute %d of relation \"%s\" does not exist",
-				 tle->resno, get_rel_name(context->parent_reloid));
-		childvar = (Var *) list_nth(context->translated_vars, tle->resno - 1);
+				 parentattno, get_rel_name(context->parent_reloid));
+		childvar = (Var *) list_nth(context->translated_vars, parentattno - 1);
 		if (childvar == NULL || !IsA(childvar, Var))
 			elog(ERROR, "attribute %d of relation \"%s\" does not exist",
-				 tle->resno, get_rel_name(context->parent_reloid));
+				 parentattno, get_rel_name(context->parent_reloid));
 
-		if (tle->resno != childvar->varattno)
-		{
-			tle->resno = childvar->varattno;
-			changed_it = true;
-		}
+		result = lappend_int(result, childvar->varattno);
 	}
+	return result;
+}
 
-	/*
-	 * If we changed anything, re-sort the tlist by resno, and make sure
-	 * resjunk entries have resnos above the last real resno.  The sort
-	 * algorithm is a bit stupid, but for such a seldom-taken path, small is
-	 * probably better than fast.
-	 */
-	if (!changed_it)
-		return tlist;
+/*
+ * adjust_inherited_attnums_multilevel
+ *	  As above, but traverse multiple inheritance levels as needed.
+ */
+List *
+adjust_inherited_attnums_multilevel(PlannerInfo *root, List *attnums,
+									Index child_relid, Index top_parent_relid)
+{
+	AppendRelInfo *appinfo = root->append_rel_array[child_relid];
 
-	new_tlist = NIL;
-	more = true;
-	for (attrno = 1; more; attrno++)
-	{
-		more = false;
-		foreach(tl, tlist)
-		{
-			TargetEntry *tle = (TargetEntry *) lfirst(tl);
+	if (!appinfo)
+		elog(ERROR, "child rel %d not found in append_rel_array", child_relid);
 
-			if (tle->resjunk)
-				continue;		/* ignore junk items */
+	/* Recurse if immediate parent is not the top parent. */
+	if (appinfo->parent_relid != top_parent_relid)
+		attnums = adjust_inherited_attnums_multilevel(root, attnums,
+													  appinfo->parent_relid,
+													  top_parent_relid);
 
-			if (tle->resno == attrno)
-				new_tlist = lappend(new_tlist, tle);
-			else if (tle->resno > attrno)
-				more = true;
-		}
-	}
+	/* Now translate for this child */
+	return adjust_inherited_attnums(attnums, appinfo);
+}
 
-	foreach(tl, tlist)
+/*
+ * get_translated_update_targetlist
+ *	  Get the processed_tlist of an UPDATE query, translated as needed to
+ *	  match a child target relation.
+ *
+ * Optionally also return the list of target column numbers translated
+ * to this target relation.  (The resnos in processed_tlist MUST NOT be
+ * relied on for this purpose.)
+ */
+void
+get_translated_update_targetlist(PlannerInfo *root, Index relid,
+								 List **processed_tlist, List **update_colnos)
+{
+	/* This is pretty meaningless for commands other than UPDATE. */
+	Assert(root->parse->commandType == CMD_UPDATE);
+	if (relid == root->parse->resultRelation)
 	{
-		TargetEntry *tle = (TargetEntry *) lfirst(tl);
-
-		if (!tle->resjunk)
-			continue;			/* here, ignore non-junk items */
-
-		tle->resno = attrno;
-		new_tlist = lappend(new_tlist, tle);
-		attrno++;
+		/*
+		 * Non-inheritance case, so it's easy.  The caller might be expecting
+		 * a tree it can scribble on, though, so copy.
+		 */
+		*processed_tlist = copyObject(root->processed_tlist);
+		if (update_colnos)
+			*update_colnos = copyObject(root->update_colnos);
+	}
+	else
+	{
+		Assert(bms_is_member(relid, root->all_result_relids));
+		*processed_tlist = (List *)
+			adjust_appendrel_attrs_multilevel(root,
+											  (Node *) root->processed_tlist,
+											  bms_make_singleton(relid),
+											  bms_make_singleton(root->parse->resultRelation));
+		if (update_colnos)
+			*update_colnos =
+				adjust_inherited_attnums_multilevel(root, root->update_colnos,
+													relid,
+													root->parse->resultRelation);
 	}
-
-	return new_tlist;
 }
 
 /*
@@ -746,3 +733,270 @@ find_appinfos_by_relids(PlannerInfo *root, Relids relids, int *nappinfos)
 	}
 	return appinfos;
 }
+
+
+/*****************************************************************************
+ *
+ *		ROW-IDENTITY VARIABLE MANAGEMENT
+ *
+ * This code lacks a good home, perhaps.  We choose to keep it here because
+ * adjust_appendrel_attrs_mutator() is its principal co-conspirator.  That
+ * function does most of what is needed to expand ROWID_VAR Vars into the
+ * right things.
+ *
+ *****************************************************************************/
+
+/*
+ * add_row_identity_var
+ *	  Register a row-identity column to be used in UPDATE/DELETE.
+ *
+ * The Var must be equal(), aside from varno, to any other row-identity
+ * column with the same rowid_name.  Thus, for example, "wholerow"
+ * row identities had better use vartype == RECORDOID.
+ *
+ * rtindex is currently redundant with rowid_var->varno, but we specify
+ * it as a separate parameter in case this is ever generalized to support
+ * non-Var expressions.  (We could reasonably handle expressions over
+ * Vars of the specified rtindex, but for now that seems unnecessary.)
+ */
+void
+add_row_identity_var(PlannerInfo *root, Var *orig_var,
+					 Index rtindex, const char *rowid_name)
+{
+	TargetEntry *tle;
+	Var		   *rowid_var;
+	RowIdentityVarInfo *ridinfo;
+	ListCell   *lc;
+
+	/* For now, the argument must be just a Var of the given rtindex */
+	Assert(IsA(orig_var, Var));
+	Assert(orig_var->varno == rtindex);
+	Assert(orig_var->varlevelsup == 0);
+
+	/*
+	 * If we're doing non-inherited UPDATE/DELETE, there's little need for
+	 * ROWID_VAR shenanigans.  Just shove the presented Var into the
+	 * processed_tlist, and we're done.
+	 */
+	if (rtindex == root->parse->resultRelation)
+	{
+		tle = makeTargetEntry((Expr *) orig_var,
+							  list_length(root->processed_tlist) + 1,
+							  pstrdup(rowid_name),
+							  true);
+		root->processed_tlist = lappend(root->processed_tlist, tle);
+		return;
+	}
+
+	/*
+	 * Otherwise, rtindex should reference a leaf target relation that's being
+	 * added to the query during expand_inherited_rtentry().
+	 */
+	Assert(bms_is_member(rtindex, root->leaf_result_relids));
+	Assert(root->append_rel_array[rtindex] != NULL);
+
+	/*
+	 * We have to find a matching RowIdentityVarInfo, or make one if there is
+	 * none.  To allow using equal() to match the vars, change the varno to
+	 * ROWID_VAR, leaving all else alone.
+	 */
+	rowid_var = copyObject(orig_var);
+	/* This could eventually become ChangeVarNodes() */
+	rowid_var->varno = ROWID_VAR;
+
+	/* Look for an existing row-id column of the same name */
+	foreach(lc, root->row_identity_vars)
+	{
+		ridinfo = (RowIdentityVarInfo *) lfirst(lc);
+		if (strcmp(rowid_name, ridinfo->rowidname) != 0)
+			continue;
+		if (equal(rowid_var, ridinfo->rowidvar))
+		{
+			/* Found a match; we need only record that rtindex needs it too */
+			ridinfo->rowidrels = bms_add_member(ridinfo->rowidrels, rtindex);
+			return;
+		}
+		else
+		{
+			/* Ooops, can't handle this */
+			elog(ERROR, "conflicting uses of row-identity name \"%s\"",
+				 rowid_name);
+		}
+	}
+
+	/* No request yet, so add a new RowIdentityVarInfo */
+	ridinfo = makeNode(RowIdentityVarInfo);
+	ridinfo->rowidvar = copyObject(rowid_var);
+	/* for the moment, estimate width using just the datatype info */
+	ridinfo->rowidwidth = get_typavgwidth(exprType((Node *) rowid_var),
+										  exprTypmod((Node *) rowid_var));
+	ridinfo->rowidname = pstrdup(rowid_name);
+	ridinfo->rowidrels = bms_make_singleton(rtindex);
+
+	root->row_identity_vars = lappend(root->row_identity_vars, ridinfo);
+
+	/* Change rowid_var into a reference to this row_identity_vars entry */
+	rowid_var->varattno = list_length(root->row_identity_vars);
+
+	/* Push the ROWID_VAR reference variable into processed_tlist */
+	tle = makeTargetEntry((Expr *) rowid_var,
+						  list_length(root->processed_tlist) + 1,
+						  pstrdup(rowid_name),
+						  true);
+	root->processed_tlist = lappend(root->processed_tlist, tle);
+}
+
+/*
+ * add_row_identity_columns
+ *
+ * This function adds the row identity columns needed by the core code.
+ * FDWs might call add_row_identity_var() for themselves to add nonstandard
+ * columns.  (Duplicate requests are fine.)
+ */
+void
+add_row_identity_columns(PlannerInfo *root, Index rtindex,
+						 RangeTblEntry *target_rte,
+						 Relation target_relation)
+{
+	CmdType		commandType = root->parse->commandType;
+	char		relkind = target_relation->rd_rel->relkind;
+	Var		   *var;
+
+	Assert(commandType == CMD_UPDATE || commandType == CMD_DELETE);
+
+	if (relkind == RELKIND_RELATION ||
+		relkind == RELKIND_MATVIEW ||
+		relkind == RELKIND_PARTITIONED_TABLE)
+	{
+		/*
+		 * Emit CTID so that executor can find the row to update or delete.
+		 */
+		var = makeVar(rtindex,
+					  SelfItemPointerAttributeNumber,
+					  TIDOID,
+					  -1,
+					  InvalidOid,
+					  0);
+		add_row_identity_var(root, var, rtindex, "ctid");
+	}
+	else if (relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/*
+		 * Let the foreign table's FDW add whatever junk TLEs it wants.
+		 */
+		FdwRoutine *fdwroutine;
+
+		fdwroutine = GetFdwRoutineForRelation(target_relation, false);
+
+		if (fdwroutine->AddForeignUpdateTargets != NULL)
+			fdwroutine->AddForeignUpdateTargets(root, rtindex,
+												target_rte, target_relation);
+
+		/*
+		 * For UPDATE, we need to make the FDW fetch unchanged columns by
+		 * asking it to fetch a whole-row Var.  That's because the top-level
+		 * targetlist only contains entries for changed columns, but
+		 * ExecUpdate will need to build the complete new tuple.  (Actually,
+		 * we only really need this in UPDATEs that are not pushed to the
+		 * remote side, but it's hard to tell if that will be the case at the
+		 * point when this function is called.)
+		 *
+		 * We will also need the whole row if there are any row triggers, so
+		 * that the executor will have the "old" row to pass to the trigger.
+		 * Alas, this misses system columns.
+		 */
+		if (commandType == CMD_UPDATE ||
+			(target_relation->trigdesc &&
+			 (target_relation->trigdesc->trig_delete_after_row ||
+			  target_relation->trigdesc->trig_delete_before_row)))
+		{
+			var = makeVar(rtindex,
+						  InvalidAttrNumber,
+						  RECORDOID,
+						  -1,
+						  InvalidOid,
+						  0);
+			add_row_identity_var(root, var, rtindex, "wholerow");
+		}
+	}
+}
+
+/*
+ * distribute_row_identity_vars
+ *
+ * After we have finished identifying all the row identity columns
+ * needed by an inherited UPDATE/DELETE query, make sure that these
+ * columns will be generated by all the target relations.
+ *
+ * This is more or less like what build_base_rel_tlists() does,
+ * except that it would not understand what to do with ROWID_VAR Vars.
+ * Since that function runs before inheritance relations are expanded,
+ * it will never see any such Vars anyway.
+ */
+void
+distribute_row_identity_vars(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	int			result_relation = parse->resultRelation;
+	RangeTblEntry *target_rte;
+	RelOptInfo *target_rel;
+	ListCell   *lc;
+
+	/* There's nothing to do if this isn't an inherited UPDATE/DELETE. */
+	if (parse->commandType != CMD_UPDATE && parse->commandType != CMD_DELETE)
+	{
+		Assert(root->row_identity_vars == NIL);
+		return;
+	}
+	target_rte = rt_fetch(result_relation, parse->rtable);
+	if (!target_rte->inh)
+	{
+		Assert(root->row_identity_vars == NIL);
+		return;
+	}
+
+	/*
+	 * Ordinarily, we expect that leaf result relation(s) will have added some
+	 * ROWID_VAR Vars to the query.  However, it's possible that constraint
+	 * exclusion suppressed every leaf relation.  The executor will get upset
+	 * if the plan has no row identity columns at all, even though it will
+	 * certainly process no rows.  Handle this edge case by re-opening the top
+	 * result relation and adding the row identity columns it would have used,
+	 * as preprocess_targetlist() would have done if it weren't marked "inh".
+	 * (This is a bit ugly, but it seems better to confine the ugliness and
+	 * extra cycles to this unusual corner case.)  We needn't worry about
+	 * fixing the rel's reltarget, as that won't affect the finished plan.
+	 */
+	if (root->row_identity_vars == NIL)
+	{
+		Relation	target_relation;
+
+		target_relation = table_open(target_rte->relid, NoLock);
+		add_row_identity_columns(root, result_relation,
+								 target_rte, target_relation);
+		table_close(target_relation, NoLock);
+		return;
+	}
+
+	/*
+	 * Dig through the processed_tlist to find the ROWID_VAR reference Vars,
+	 * and forcibly copy them into the reltarget list of the topmost target
+	 * relation.  That's sufficient because they'll be copied to the
+	 * individual leaf target rels (with appropriate translation) later,
+	 * during appendrel expansion --- see set_append_rel_size().
+	 */
+	target_rel = find_base_rel(root, result_relation);
+
+	foreach(lc, root->processed_tlist)
+	{
+		TargetEntry *tle = lfirst(lc);
+		Var		   *var = (Var *) tle->expr;
+
+		if (var && IsA(var, Var) && var->varno == ROWID_VAR)
+		{
+			target_rel->reltarget->exprs =
+				lappend(target_rel->reltarget->exprs, copyObject(var));
+			/* reltarget cost and width will be computed later */
+		}
+	}
+}
diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c
index be1c9ddd96..13f67ab744 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -219,6 +219,10 @@ expand_inherited_rtentry(PlannerInfo *root, RelOptInfo *rel,
 	 * targetlist and update parent rel's reltarget.  This should match what
 	 * preprocess_targetlist() would have added if the mark types had been
 	 * requested originally.
+	 *
+	 * (Someday it might be useful to fold these resjunk columns into the
+	 * row-identity-column management used for UPDATE/DELETE.  Today is not
+	 * that day, however.)
 	 */
 	if (oldrc)
 	{
@@ -585,6 +589,46 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte,
 
 		root->rowMarks = lappend(root->rowMarks, childrc);
 	}
+
+	/*
+	 * If we are creating a child of the query target relation (only possible
+	 * in UPDATE/DELETE), add it to all_result_relids, as well as
+	 * leaf_result_relids if appropriate, and make sure that we generate
+	 * required row-identity data.
+	 */
+	if (bms_is_member(parentRTindex, root->all_result_relids))
+	{
+		/* OK, record the child as a result rel too. */
+		root->all_result_relids = bms_add_member(root->all_result_relids,
+												 childRTindex);
+
+		/* Non-leaf partitions don't need any row identity info. */
+		if (childrte->relkind != RELKIND_PARTITIONED_TABLE)
+		{
+			Var		   *rrvar;
+
+			root->leaf_result_relids = bms_add_member(root->leaf_result_relids,
+													  childRTindex);
+
+			/*
+			 * If we have any child target relations, assume they all need to
+			 * generate a junk "tableoid" column.  (If only one child survives
+			 * pruning, we wouldn't really need this, but it's not worth
+			 * thrashing about to avoid it.)
+			 */
+			rrvar = makeVar(childRTindex,
+							TableOidAttributeNumber,
+							OIDOID,
+							-1,
+							InvalidOid,
+							0);
+			add_row_identity_var(root, rrvar, childRTindex, "tableoid");
+
+			/* Register any row-identity columns needed by this child. */
+			add_row_identity_columns(root, childRTindex,
+									 childrte, childrel);
+		}
+	}
 }
 
 /*
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index a97929c13f..64edf7e9fc 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3539,6 +3539,7 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
  *	  Creates a pathnode that represents performing INSERT/UPDATE/DELETE mods
  *
  * 'rel' is the parent relation associated with the result
+ * 'subpath' is a Path producing source data
  * 'operation' is the operation type
  * 'canSetTag' is true if we set the command tag/es_processed
  * 'nominalRelation' is the parent RT index for use of EXPLAIN
@@ -3546,8 +3547,6 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
  * 'partColsUpdated' is true if any partitioning columns are being updated,
  *		either from the target relation or a descendent partitioned table.
  * 'resultRelations' is an integer list of actual RT indexes of target rel(s)
- * 'subpaths' is a list of Path(s) producing source data (one per rel)
- * 'subroots' is a list of PlannerInfo structs (one per rel)
  * 'updateColnosLists' is a list of UPDATE target column number lists
  *		(one sublist per rel); or NIL if not an UPDATE
  * 'withCheckOptionLists' is a list of WCO lists (one per rel)
@@ -3558,22 +3557,18 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
  */
 ModifyTablePath *
 create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
+						Path *subpath,
 						CmdType operation, bool canSetTag,
 						Index nominalRelation, Index rootRelation,
 						bool partColsUpdated,
-						List *resultRelations, List *subpaths,
-						List *subroots,
+						List *resultRelations,
 						List *updateColnosLists,
 						List *withCheckOptionLists, List *returningLists,
 						List *rowMarks, OnConflictExpr *onconflict,
 						int epqParam)
 {
 	ModifyTablePath *pathnode = makeNode(ModifyTablePath);
-	double		total_size;
-	ListCell   *lc;
 
-	Assert(list_length(resultRelations) == list_length(subpaths));
-	Assert(list_length(resultRelations) == list_length(subroots));
 	Assert(operation == CMD_UPDATE ?
 		   list_length(resultRelations) == list_length(updateColnosLists) :
 		   updateColnosLists == NIL);
@@ -3594,7 +3589,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 	pathnode->path.pathkeys = NIL;
 
 	/*
-	 * Compute cost & rowcount as sum of subpath costs & rowcounts.
+	 * Compute cost & rowcount as subpath cost & rowcount (if RETURNING)
 	 *
 	 * Currently, we don't charge anything extra for the actual table
 	 * modification work, nor for the WITH CHECK OPTIONS or RETURNING
@@ -3603,42 +3598,33 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 	 * costs to change any higher-level planning choices.  But we might want
 	 * to make it look better sometime.
 	 */
-	pathnode->path.startup_cost = 0;
-	pathnode->path.total_cost = 0;
-	pathnode->path.rows = 0;
-	total_size = 0;
-	foreach(lc, subpaths)
+	pathnode->path.startup_cost = subpath->startup_cost;
+	pathnode->path.total_cost = subpath->total_cost;
+	if (returningLists != NIL)
 	{
-		Path	   *subpath = (Path *) lfirst(lc);
+		pathnode->path.rows = subpath->rows;
 
-		if (lc == list_head(subpaths))	/* first node? */
-			pathnode->path.startup_cost = subpath->startup_cost;
-		pathnode->path.total_cost += subpath->total_cost;
-		if (returningLists != NIL)
-		{
-			pathnode->path.rows += subpath->rows;
-			total_size += subpath->pathtarget->width * subpath->rows;
-		}
+		/*
+		 * Set width to match the subpath output.  XXX this is totally wrong:
+		 * we should return an average of the RETURNING tlist widths.  But
+		 * it's what happened historically, and improving it is a task for
+		 * another day.  (Again, it's mostly window dressing.)
+		 */
+		pathnode->path.pathtarget->width = subpath->pathtarget->width;
+	}
+	else
+	{
+		pathnode->path.rows = 0;
+		pathnode->path.pathtarget->width = 0;
 	}
 
-	/*
-	 * Set width to the average width of the subpath outputs.  XXX this is
-	 * totally wrong: we should return an average of the RETURNING tlist
-	 * widths.  But it's what happened historically, and improving it is a task
-	 * for another day.
-	 */
-	if (pathnode->path.rows > 0)
-		total_size /= pathnode->path.rows;
-	pathnode->path.pathtarget->width = rint(total_size);
-
+	pathnode->subpath = subpath;
 	pathnode->operation = operation;
 	pathnode->canSetTag = canSetTag;
 	pathnode->nominalRelation = nominalRelation;
 	pathnode->rootRelation = rootRelation;
 	pathnode->partColsUpdated = partColsUpdated;
 	pathnode->resultRelations = resultRelations;
-	pathnode->subpaths = subpaths;
-	pathnode->subroots = subroots;
 	pathnode->updateColnosLists = updateColnosLists;
 	pathnode->withCheckOptionLists = withCheckOptionLists;
 	pathnode->returningLists = returningLists;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 6c39bf893f..d0fb3b6834 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1453,18 +1453,11 @@ relation_excluded_by_constraints(PlannerInfo *root,
 
 			/*
 			 * When constraint_exclusion is set to 'partition' we only handle
-			 * appendrel members.  Normally, they are RELOPT_OTHER_MEMBER_REL
-			 * relations, but we also consider inherited target relations as
-			 * appendrel members for the purposes of constraint exclusion
-			 * (since, indeed, they were appendrel members earlier in
-			 * inheritance_planner).
-			 *
-			 * In both cases, partition pruning was already applied, so there
-			 * is no need to consider the rel's partition constraints here.
+			 * appendrel members.  Partition pruning has already been applied,
+			 * so there is no need to consider the rel's partition constraints
+			 * here.
 			 */
-			if (rel->reloptkind == RELOPT_OTHER_MEMBER_REL ||
-				(rel->relid == root->parse->resultRelation &&
-				 root->inhTargetKind != INHKIND_NONE))
+			if (rel->reloptkind == RELOPT_OTHER_MEMBER_REL)
 				break;			/* appendrel member, so process it */
 			return false;
 
@@ -1477,9 +1470,7 @@ relation_excluded_by_constraints(PlannerInfo *root,
 			 * its partition constraints haven't been considered yet, so
 			 * include them in the processing here.
 			 */
-			if (rel->reloptkind == RELOPT_BASEREL &&
-				!(rel->relid == root->parse->resultRelation &&
-				  root->inhTargetKind != INHKIND_NONE))
+			if (rel->reloptkind == RELOPT_BASEREL)
 				include_partition = true;
 			break;				/* always try to exclude */
 	}
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 345c877aeb..e105a4d5f1 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -977,8 +977,6 @@ build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel,
 	foreach(vars, input_rel->reltarget->exprs)
 	{
 		Var		   *var = (Var *) lfirst(vars);
-		RelOptInfo *baserel;
-		int			ndx;
 
 		/*
 		 * Ignore PlaceHolderVars in the input tlists; we'll make our own
@@ -996,17 +994,35 @@ build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel,
 			elog(ERROR, "unexpected node type in rel targetlist: %d",
 				 (int) nodeTag(var));
 
-		/* Get the Var's original base rel */
-		baserel = find_base_rel(root, var->varno);
-
-		/* Is it still needed above this joinrel? */
-		ndx = var->varattno - baserel->min_attr;
-		if (bms_nonempty_difference(baserel->attr_needed[ndx], relids))
+		if (var->varno == ROWID_VAR)
 		{
-			/* Yup, add it to the output */
-			joinrel->reltarget->exprs = lappend(joinrel->reltarget->exprs, var);
+			/* UPDATE/DELETE row identity vars are always needed */
+			RowIdentityVarInfo *ridinfo = (RowIdentityVarInfo *)
+			list_nth(root->row_identity_vars, var->varattno - 1);
+
+			joinrel->reltarget->exprs = lappend(joinrel->reltarget->exprs,
+												var);
 			/* Vars have cost zero, so no need to adjust reltarget->cost */
-			joinrel->reltarget->width += baserel->attr_widths[ndx];
+			joinrel->reltarget->width += ridinfo->rowidwidth;
+		}
+		else
+		{
+			RelOptInfo *baserel;
+			int			ndx;
+
+			/* Get the Var's original base rel */
+			baserel = find_base_rel(root, var->varno);
+
+			/* Is it still needed above this joinrel? */
+			ndx = var->varattno - baserel->min_attr;
+			if (bms_nonempty_difference(baserel->attr_needed[ndx], relids))
+			{
+				/* Yup, add it to the output */
+				joinrel->reltarget->exprs = lappend(joinrel->reltarget->exprs,
+													var);
+				/* Vars have cost zero, so no need to adjust reltarget->cost */
+				joinrel->reltarget->width += baserel->attr_widths[ndx];
+			}
 		}
 	}
 }
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index f9175987f8..92661abae2 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -705,16 +705,9 @@ adjustJoinTreeList(Query *parsetree, bool removert, int rt_index)
  *
  * We must do items 1,2,3 before firing rewrite rules, else rewritten
  * references to NEW.foo will produce wrong or incomplete results.  Item 4
- * is not needed for rewriting, but will be needed by the planner, and we
+ * is not needed for rewriting, but it is helpful for the planner, and we
  * can do it essentially for free while handling the other items.
  *
- * Note that for an inheritable UPDATE, this processing is only done once,
- * using the parent relation as reference.  It must not do anything that
- * will not be correct when transposed to the child relation(s).  (Step 4
- * is incorrect by this light, since child relations might have different
- * column ordering, but the planner will fix things by re-sorting the tlist
- * for each child.)
- *
  * If values_rte is non-NULL (i.e., we are doing a multi-row INSERT using
  * values from a VALUES RTE), we populate *unused_values_attrnos with the
  * attribute numbers of any unused columns from the VALUES RTE.  This can
@@ -1607,94 +1600,6 @@ rewriteValuesRTE(Query *parsetree, RangeTblEntry *rte, int rti,
 }
 
 
-/*
- * rewriteTargetListUD - rewrite UPDATE/DELETE targetlist as needed
- *
- * This function adds a "junk" TLE that is needed to allow the executor to
- * find the original row for the update or delete.  When the target relation
- * is a regular table, the junk TLE emits the ctid attribute of the original
- * row.  When the target relation is a foreign table, we let the FDW decide
- * what to add.
- *
- * We used to do this during RewriteQuery(), but now that inheritance trees
- * can contain a mix of regular and foreign tables, we must postpone it till
- * planning, after the inheritance tree has been expanded.  In that way we
- * can do the right thing for each child table.
- */
-void
-rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
-					Relation target_relation)
-{
-	Var		   *var = NULL;
-	const char *attrname;
-	TargetEntry *tle;
-
-	if (target_relation->rd_rel->relkind == RELKIND_RELATION ||
-		target_relation->rd_rel->relkind == RELKIND_MATVIEW ||
-		target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
-	{
-		/*
-		 * Emit CTID so that executor can find the row to update or delete.
-		 */
-		var = makeVar(parsetree->resultRelation,
-					  SelfItemPointerAttributeNumber,
-					  TIDOID,
-					  -1,
-					  InvalidOid,
-					  0);
-
-		attrname = "ctid";
-	}
-	else if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
-	{
-		/*
-		 * Let the foreign table's FDW add whatever junk TLEs it wants.
-		 */
-		FdwRoutine *fdwroutine;
-
-		fdwroutine = GetFdwRoutineForRelation(target_relation, false);
-
-		if (fdwroutine->AddForeignUpdateTargets != NULL)
-			fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
-												target_relation);
-
-		/*
-		 * For UPDATE, we need to make the FDW fetch unchanged columns by
-		 * asking it to fetch a whole-row Var.  That's because the top-level
-		 * targetlist only contains entries for changed columns.  (Actually,
-		 * we only really need this for UPDATEs that are not pushed to the
-		 * remote side, but it's hard to tell if that will be the case at the
-		 * point when this function is called.)
-		 *
-		 * We will also need the whole row if there are any row triggers, so
-		 * that the executor will have the "old" row to pass to the trigger.
-		 * Alas, this misses system columns.
-		 */
-		if (parsetree->commandType == CMD_UPDATE ||
-			(target_relation->trigdesc &&
-			 (target_relation->trigdesc->trig_delete_after_row ||
-			  target_relation->trigdesc->trig_delete_before_row)))
-		{
-			var = makeWholeRowVar(target_rte,
-								  parsetree->resultRelation,
-								  0,
-								  false);
-
-			attrname = "wholerow";
-		}
-	}
-
-	if (var != NULL)
-	{
-		tle = makeTargetEntry((Expr *) var,
-							  list_length(parsetree->targetList) + 1,
-							  pstrdup(attrname),
-							  true);
-
-		parsetree->targetList = lappend(parsetree->targetList, tle);
-	}
-}
-
 /*
  * Record in target_rte->extraUpdatedCols the indexes of any generated columns
  * that depend on any columns mentioned in target_rte->updatedCols.
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f0de2a25c9..03c22c80c3 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -4572,16 +4572,12 @@ set_deparse_plan(deparse_namespace *dpns, Plan *plan)
 	 * We special-case Append and MergeAppend to pretend that the first child
 	 * plan is the OUTER referent; we have to interpret OUTER Vars in their
 	 * tlists according to one of the children, and the first one is the most
-	 * natural choice.  Likewise special-case ModifyTable to pretend that the
-	 * first child plan is the OUTER referent; this is to support RETURNING
-	 * lists containing references to non-target relations.
+	 * natural choice.
 	 */
 	if (IsA(plan, Append))
 		dpns->outer_plan = linitial(((Append *) plan)->appendplans);
 	else if (IsA(plan, MergeAppend))
 		dpns->outer_plan = linitial(((MergeAppend *) plan)->mergeplans);
-	else if (IsA(plan, ModifyTable))
-		dpns->outer_plan = linitial(((ModifyTable *) plan)->plans);
 	else
 		dpns->outer_plan = outerPlan(plan);
 
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 248f78da45..bd68fd5f8c 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -65,7 +65,8 @@ typedef void (*GetForeignUpperPaths_function) (PlannerInfo *root,
 											   RelOptInfo *output_rel,
 											   void *extra);
 
-typedef void (*AddForeignUpdateTargets_function) (Query *parsetree,
+typedef void (*AddForeignUpdateTargets_function) (PlannerInfo *root,
+												  Index rtindex,
 												  RangeTblEntry *target_rte,
 												  Relation target_relation);
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 7af6d48525..d5f96609d8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -422,7 +422,7 @@ typedef struct ResultRelInfo
 	 * For UPDATE/DELETE result relations, the attribute number of the row
 	 * identity junk attribute in the source plan's output tuples
 	 */
-	AttrNumber		ri_RowIdAttNo;
+	AttrNumber	ri_RowIdAttNo;
 
 	/* Projection to generate new tuple in an INSERT/UPDATE */
 	ProjectionInfo *ri_projectNew;
@@ -666,10 +666,7 @@ typedef struct ExecRowMark
  * Each LockRows and ModifyTable node keeps a list of the rowmarks it needs to
  * deal with.  In addition to a pointer to the related entry in es_rowmarks,
  * this struct carries the column number(s) of the resjunk columns associated
- * with the rowmark (see comments for PlanRowMark for more detail).  In the
- * case of ModifyTable, there has to be a separate ExecAuxRowMark list for
- * each child plan, because the resjunk columns could be at different physical
- * column positions in different subplans.
+ * with the rowmark (see comments for PlanRowMark for more detail).
  */
 typedef struct ExecAuxRowMark
 {
@@ -1071,9 +1068,8 @@ typedef struct PlanState
  * EvalPlanQualSlot), and/or found using the rowmark mechanism (non-locking
  * rowmarks by the EPQ machinery itself, locking ones by the caller).
  *
- * While the plan to be checked may be changed using EvalPlanQualSetPlan() -
- * e.g. so all source plans for a ModifyTable node can be processed - all such
- * plans need to share the same EState.
+ * While the plan to be checked may be changed using EvalPlanQualSetPlan(),
+ * all such plans need to share the same EState.
  */
 typedef struct EPQState
 {
@@ -1167,23 +1163,31 @@ typedef struct ModifyTableState
 	CmdType		operation;		/* INSERT, UPDATE, or DELETE */
 	bool		canSetTag;		/* do we set the command tag/es_processed? */
 	bool		mt_done;		/* are we done? */
-	PlanState **mt_plans;		/* subplans (one per target rel) */
-	int			mt_nplans;		/* number of plans in the array */
-	int			mt_whichplan;	/* which one is being executed (0..n-1) */
-	TupleTableSlot **mt_scans;	/* input tuple corresponding to underlying
-								 * plans */
-	ResultRelInfo *resultRelInfo;	/* per-subplan target relations */
+	int			mt_nrels;		/* number of entries in resultRelInfo[] */
+	ResultRelInfo *resultRelInfo;	/* info about target relation(s) */
 
 	/*
 	 * Target relation mentioned in the original statement, used to fire
-	 * statement-level triggers and as the root for tuple routing.
+	 * statement-level triggers and as the root for tuple routing.  (This
+	 * might point to one of the resultRelInfo[] entries, but it can also be a
+	 * distinct struct.)
 	 */
 	ResultRelInfo *rootResultRelInfo;
 
-	List	  **mt_arowmarks;	/* per-subplan ExecAuxRowMark lists */
 	EPQState	mt_epqstate;	/* for evaluating EvalPlanQual rechecks */
 	bool		fireBSTriggers; /* do we need to fire stmt triggers? */
 
+	/*
+	 * These fields are used for inherited UPDATE and DELETE, to track which
+	 * target relation a given tuple is from.  If there are a lot of target
+	 * relations, we use a hash table to translate table OIDs to
+	 * resultRelInfo[] indexes; otherwise mt_resultOidHash is NULL.
+	 */
+	int			mt_resultOidAttno;	/* resno of "tableoid" junk attr */
+	Oid			mt_lastResultOid;	/* last-seen value of tableoid */
+	int			mt_lastResultIndex; /* corresponding index in resultRelInfo[] */
+	HTAB	   *mt_resultOidHash;	/* optional hash table to speed lookups */
+
 	/*
 	 * Slot for storing tuples in the root partitioned table's rowtype during
 	 * an UPDATE of a partitioned table.
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index e22df890ef..16c750da02 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -270,6 +270,7 @@ typedef enum NodeTag
 	T_PlaceHolderVar,
 	T_SpecialJoinInfo,
 	T_AppendRelInfo,
+	T_RowIdentityVarInfo,
 	T_PlaceHolderInfo,
 	T_MinMaxAggInfo,
 	T_PlannerParamItem,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index bed9f4da09..c37d1259cf 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -77,18 +77,6 @@ typedef enum UpperRelationKind
 	/* NB: UPPERREL_FINAL must be last enum entry; it's used to size arrays */
 } UpperRelationKind;
 
-/*
- * This enum identifies which type of relation is being planned through the
- * inheritance planner.  INHKIND_NONE indicates the inheritance planner
- * was not used.
- */
-typedef enum InheritanceKind
-{
-	INHKIND_NONE,
-	INHKIND_INHERITED,
-	INHKIND_PARTITIONED
-} InheritanceKind;
-
 /*----------
  * PlannerGlobal
  *		Global information for planning/optimization
@@ -276,6 +264,17 @@ struct PlannerInfo
 
 	List	   *join_info_list; /* list of SpecialJoinInfos */
 
+	/*
+	 * all_result_relids is empty for SELECT, otherwise it contains at least
+	 * parse->resultRelation.  For UPDATE/DELETE across an inheritance or
+	 * partitioning tree, the result rel's child relids are added.  When using
+	 * multi-level partitioning, intermediate partitioned rels are included.
+	 * leaf_result_relids is similar except that only actual result tables,
+	 * not partitioned tables, are included in it.
+	 */
+	Relids		all_result_relids;	/* set of all result relids */
+	Relids		leaf_result_relids; /* set of all leaf relids */
+
 	/*
 	 * Note: for AppendRelInfos describing partitions of a partitioned table,
 	 * we guarantee that partitions that come earlier in the partitioned
@@ -283,6 +282,8 @@ struct PlannerInfo
 	 */
 	List	   *append_rel_list;	/* list of AppendRelInfos */
 
+	List	   *row_identity_vars;	/* list of RowIdentityVarInfos */
+
 	List	   *rowMarks;		/* list of PlanRowMarks */
 
 	List	   *placeholder_list;	/* list of PlaceHolderInfos */
@@ -322,7 +323,8 @@ struct PlannerInfo
 	 * For UPDATE, processed_tlist remains in the order the user wrote the
 	 * assignments.  This list contains the target table's attribute numbers
 	 * to which the first N entries of processed_tlist are to be assigned.
-	 * (Any additional entries in processed_tlist must be resjunk.)
+	 * (Any additional entries in processed_tlist must be resjunk.)  DO NOT
+	 * use the resnos in processed_tlist to identify the UPDATE targets.
 	 */
 	List	   *update_colnos;
 
@@ -341,9 +343,6 @@ struct PlannerInfo
 	Index		qual_security_level;	/* minimum security_level for quals */
 	/* Note: qual_security_level is zero if there are no securityQuals */
 
-	InheritanceKind inhTargetKind;	/* indicates if the target relation is an
-									 * inheritance child or partition or a
-									 * partitioned table */
 	bool		hasJoinRTEs;	/* true if any RTEs are RTE_JOIN kind */
 	bool		hasLateralRTEs; /* true if any RTEs are marked LATERAL */
 	bool		hasHavingQual;	/* true if havingQual was non-null */
@@ -1833,20 +1832,19 @@ typedef struct LockRowsPath
  * ModifyTablePath represents performing INSERT/UPDATE/DELETE modifications
  *
  * We represent most things that will be in the ModifyTable plan node
- * literally, except we have child Path(s) not Plan(s).  But analysis of the
+ * literally, except we have a child Path not Plan.  But analysis of the
  * OnConflictExpr is deferred to createplan.c, as is collection of FDW data.
  */
 typedef struct ModifyTablePath
 {
 	Path		path;
+	Path	   *subpath;		/* Path producing source data */
 	CmdType		operation;		/* INSERT, UPDATE, or DELETE */
 	bool		canSetTag;		/* do we set the command tag/es_processed? */
 	Index		nominalRelation;	/* Parent RT index for use of EXPLAIN */
 	Index		rootRelation;	/* Root RT index, if target is partitioned */
-	bool		partColsUpdated;	/* some part key in hierarchy updated */
+	bool		partColsUpdated;	/* some part key in hierarchy updated? */
 	List	   *resultRelations;	/* integer list of RT indexes */
-	List	   *subpaths;		/* Path(s) producing source data */
-	List	   *subroots;		/* per-target-table PlannerInfos */
 	List	   *updateColnosLists; /* per-target-table update_colnos lists */
 	List	   *withCheckOptionLists;	/* per-target-table WCO lists */
 	List	   *returningLists; /* per-target-table RETURNING tlists */
@@ -2303,6 +2301,34 @@ typedef struct AppendRelInfo
 	Oid			parent_reloid;	/* OID of parent relation */
 } AppendRelInfo;
 
+/*
+ * Information about a row-identity "resjunk" column in UPDATE/DELETE.
+ *
+ * In partitioned UPDATE/DELETE it's important for child partitions to share
+ * row-identity columns whenever possible, so as not to chew up too many
+ * targetlist columns.  We use these structs to track which identity columns
+ * have been requested.  In the finished plan, each of these will give rise
+ * to one resjunk entry in the targetlist of the ModifyTable's subplan node.
+ *
+ * All the Vars stored in RowIdentityVarInfos must have varno ROWID_VAR, for
+ * convenience of detecting duplicate requests.  We'll replace that, in the
+ * final plan, with the varno of the generating rel.
+ *
+ * Outside this list, a Var with varno ROWID_VAR and varattno k is a reference
+ * to the k-th element of the row_identity_vars list (k counting from 1).
+ * We add such a reference to root->processed_tlist when creating the entry,
+ * and it propagates into the plan tree from there.
+ */
+typedef struct RowIdentityVarInfo
+{
+	NodeTag		type;
+
+	Var		   *rowidvar;		/* Var to be evaluated (but varno=ROWID_VAR) */
+	int32		rowidwidth;		/* estimated average width */
+	char	   *rowidname;		/* name of the resjunk column */
+	Relids		rowidrels;		/* RTE indexes of target rels using this */
+} RowIdentityVarInfo;
+
 /*
  * For each distinct placeholder expression generated during planning, we
  * store a PlaceHolderInfo node in the PlannerInfo node's placeholder_list.
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 7d74bd92b8..f371390f7f 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -196,7 +196,7 @@ typedef struct ProjectSet
 
 /* ----------------
  *	 ModifyTable node -
- *		Apply rows produced by subplan(s) to result table(s),
+ *		Apply rows produced by outer plan to result table(s),
  *		by inserting, updating, or deleting.
  *
  * If the originally named target table is a partitioned table, both
@@ -206,7 +206,7 @@ typedef struct ProjectSet
  * EXPLAIN should claim is the INSERT/UPDATE/DELETE target.
  *
  * Note that rowMarks and epqParam are presumed to be valid for all the
- * subplan(s); they can't contain any info that varies across subplans.
+ * table(s); they can't contain any info that varies across tables.
  * ----------------
  */
 typedef struct ModifyTable
@@ -216,9 +216,8 @@ typedef struct ModifyTable
 	bool		canSetTag;		/* do we set the command tag/es_processed? */
 	Index		nominalRelation;	/* Parent RT index for use of EXPLAIN */
 	Index		rootRelation;	/* Root RT index, if target is partitioned */
-	bool		partColsUpdated;	/* some part key in hierarchy updated */
+	bool		partColsUpdated;	/* some part key in hierarchy updated? */
 	List	   *resultRelations;	/* integer list of RT indexes */
-	List	   *plans;			/* plan(s) producing source data */
 	List	   *updateColnosLists; /* per-target-table update_colnos lists */
 	List	   *withCheckOptionLists;	/* per-target-table WCO lists */
 	List	   *returningLists; /* per-target-table RETURNING tlists */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index d4ce037088..c25605ce0c 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -158,6 +158,10 @@ typedef struct Expr
  * than a heap column.  (In ForeignScan and CustomScan plan nodes, INDEX_VAR
  * is abused to signify references to columns of a custom scan tuple type.)
  *
+ * ROWID_VAR is used in the planner to identify nonce variables that carry
+ * row identity information during UPDATE/DELETE.  This value should never
+ * be seen outside the planner.
+ *
  * In the parser, varnosyn and varattnosyn are either identical to
  * varno/varattno, or they specify the column's position in an aliased JOIN
  * RTE that hides the semantic referent RTE's refname.  This is a syntactic
@@ -171,6 +175,7 @@ typedef struct Expr
 #define    INNER_VAR		65000	/* reference to inner subplan */
 #define    OUTER_VAR		65001	/* reference to outer subplan */
 #define    INDEX_VAR		65002	/* reference to index column */
+#define    ROWID_VAR		65003	/* row identity column during planning */
 
 #define IS_SPECIAL_VARNO(varno)		((varno) >= INNER_VAR)
 
@@ -1386,13 +1391,14 @@ typedef struct InferenceElem
  * column for the item; so there may be missing or out-of-order resnos.
  * It is even legal to have duplicated resnos; consider
  *		UPDATE table SET arraycol[1] = ..., arraycol[2] = ..., ...
- * The two meanings come together in the executor, because the planner
- * transforms INSERT/UPDATE tlists into a normalized form with exactly
- * one entry for each column of the destination table.  Before that's
- * happened, however, it is risky to assume that resno == position.
- * Generally get_tle_by_resno() should be used rather than list_nth()
- * to fetch tlist entries by resno, and only in SELECT should you assume
- * that resno is a unique identifier.
+ * In an INSERT, the rewriter and planner will normalize the tlist by
+ * reordering it into physical column order and filling in default values
+ * for any columns not assigned values by the original query.  In an UPDATE,
+ * after the rewriter merges multiple assignments for the same column, the
+ * planner extracts the target-column numbers into a separate "update_colnos"
+ * list, and then renumbers the tlist elements serially.  Thus, tlist resnos
+ * match ordinal position in all tlists seen by the executor; but it is wrong
+ * to assume that before planning has happened.
  *
  * resname is required to represent the correct column name in non-resjunk
  * entries of top-level SELECT targetlists, since it will be used as the
diff --git a/src/include/optimizer/appendinfo.h b/src/include/optimizer/appendinfo.h
index 4cbf8c26cc..39d04d9cc0 100644
--- a/src/include/optimizer/appendinfo.h
+++ b/src/include/optimizer/appendinfo.h
@@ -28,8 +28,23 @@ extern Node *adjust_appendrel_attrs_multilevel(PlannerInfo *root, Node *node,
 extern Relids adjust_child_relids(Relids relids, int nappinfos,
 								  AppendRelInfo **appinfos);
 extern Relids adjust_child_relids_multilevel(PlannerInfo *root, Relids relids,
-											 Relids child_relids, Relids top_parent_relids);
+											 Relids child_relids,
+											 Relids top_parent_relids);
+extern List *adjust_inherited_attnums(List *attnums, AppendRelInfo *context);
+extern List *adjust_inherited_attnums_multilevel(PlannerInfo *root,
+												 List *attnums,
+												 Index child_relid,
+												 Index top_parent_relid);
+extern void get_translated_update_targetlist(PlannerInfo *root, Index relid,
+											 List **processed_tlist,
+											 List **update_colnos);
 extern AppendRelInfo **find_appinfos_by_relids(PlannerInfo *root,
 											   Relids relids, int *nappinfos);
+extern void add_row_identity_var(PlannerInfo *root, Var *rowid_var,
+								 Index rtindex, const char *rowid_name);
+extern void add_row_identity_columns(PlannerInfo *root, Index rtindex,
+									 RangeTblEntry *target_rte,
+									 Relation target_relation);
+extern void distribute_row_identity_vars(PlannerInfo *root);
 
 #endif							/* APPENDINFO_H */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 9673a4a638..d539bc2783 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -260,11 +260,11 @@ extern LockRowsPath *create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
 										  Path *subpath, List *rowMarks, int epqParam);
 extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
 												RelOptInfo *rel,
+												Path *subpath,
 												CmdType operation, bool canSetTag,
 												Index nominalRelation, Index rootRelation,
 												bool partColsUpdated,
-												List *resultRelations, List *subpaths,
-												List *subroots,
+												List *resultRelations,
 												List *updateColnosLists,
 												List *withCheckOptionLists, List *returningLists,
 												List *rowMarks, OnConflictExpr *onconflict,
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
index f49196a4d3..b1c4065689 100644
--- a/src/include/optimizer/prep.h
+++ b/src/include/optimizer/prep.h
@@ -34,7 +34,7 @@ extern Relids get_relids_for_join(Query *query, int joinrelid);
 /*
  * prototypes for preptlist.c
  */
-extern List *preprocess_targetlist(PlannerInfo *root);
+extern void preprocess_targetlist(PlannerInfo *root);
 
 extern PlanRowMark *get_plan_rowmark(List *rowmarks, Index rtindex);
 
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 1fea1a4691..728a60c0b0 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -23,8 +23,6 @@ extern void AcquireRewriteLocks(Query *parsetree,
 								bool forUpdatePushedDown);
 
 extern Node *build_column_default(Relation rel, int attrno);
-extern void rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
-								Relation target_relation);
 
 extern void fill_extraUpdatedCols(RangeTblEntry *target_rte,
 								  Relation target_relation);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 94e43c3410..1c703c351f 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -545,27 +545,25 @@ create table some_tab_child () inherits (some_tab);
 insert into some_tab_child values(1,2);
 explain (verbose, costs off)
 update some_tab set a = a + 1 where false;
-           QUERY PLAN           
---------------------------------
+                       QUERY PLAN                       
+--------------------------------------------------------
  Update on public.some_tab
-   Update on public.some_tab
    ->  Result
-         Output: (a + 1), ctid
+         Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(5 rows)
+(4 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
 update some_tab set a = a + 1 where false returning b, a;
-           QUERY PLAN           
---------------------------------
+                       QUERY PLAN                       
+--------------------------------------------------------
  Update on public.some_tab
-   Output: b, a
-   Update on public.some_tab
+   Output: some_tab.b, some_tab.a
    ->  Result
-         Output: (a + 1), ctid
+         Output: (some_tab.a + 1), NULL::oid, NULL::tid
          One-Time Filter: false
-(6 rows)
+(5 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -670,7 +668,7 @@ explain update parted_tab set a = 2 where false;
                        QUERY PLAN                       
 --------------------------------------------------------
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
-   ->  Result  (cost=0.00..0.00 rows=0 width=0)
+   ->  Result  (cost=0.00..0.00 rows=0 width=10)
          One-Time Filter: false
 (3 rows)
 
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index ff157ceb1c..73c0f3e04b 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -212,7 +212,7 @@ explain (costs off, format json) insert into insertconflicttest values (0, 'Bilb
        "Plans": [                                                      +
          {                                                             +
            "Node Type": "Result",                                      +
-           "Parent Relationship": "Member",                            +
+           "Parent Relationship": "Outer",                             +
            "Parallel Aware": false                                     +
          }                                                             +
        ]                                                               +
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 0057f41caa..27f7525b3e 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1926,37 +1926,27 @@ WHERE EXISTS (
     FROM int4_tbl,
          LATERAL (SELECT int4_tbl.f1 FROM int8_tbl LIMIT 2) ss
     WHERE prt1_l.c IS NULL);
-                          QUERY PLAN                           
----------------------------------------------------------------
+                        QUERY PLAN                        
+----------------------------------------------------------
  Delete on prt1_l
    Delete on prt1_l_p1 prt1_l_1
    Delete on prt1_l_p3_p1 prt1_l_2
    Delete on prt1_l_p3_p2 prt1_l_3
    ->  Nested Loop Semi Join
-         ->  Seq Scan on prt1_l_p1 prt1_l_1
-               Filter: (c IS NULL)
-         ->  Nested Loop
-               ->  Seq Scan on int4_tbl
-               ->  Subquery Scan on ss
-                     ->  Limit
-                           ->  Seq Scan on int8_tbl
-   ->  Nested Loop Semi Join
-         ->  Seq Scan on prt1_l_p3_p1 prt1_l_2
-               Filter: (c IS NULL)
-         ->  Nested Loop
-               ->  Seq Scan on int4_tbl
-               ->  Subquery Scan on ss_1
-                     ->  Limit
-                           ->  Seq Scan on int8_tbl int8_tbl_1
-   ->  Nested Loop Semi Join
-         ->  Seq Scan on prt1_l_p3_p2 prt1_l_3
-               Filter: (c IS NULL)
-         ->  Nested Loop
-               ->  Seq Scan on int4_tbl
-               ->  Subquery Scan on ss_2
-                     ->  Limit
-                           ->  Seq Scan on int8_tbl int8_tbl_2
-(28 rows)
+         ->  Append
+               ->  Seq Scan on prt1_l_p1 prt1_l_1
+                     Filter: (c IS NULL)
+               ->  Seq Scan on prt1_l_p3_p1 prt1_l_2
+                     Filter: (c IS NULL)
+               ->  Seq Scan on prt1_l_p3_p2 prt1_l_3
+                     Filter: (c IS NULL)
+         ->  Materialize
+               ->  Nested Loop
+                     ->  Seq Scan on int4_tbl
+                     ->  Subquery Scan on ss
+                           ->  Limit
+                                 ->  Seq Scan on int8_tbl
+(18 rows)
 
 --
 -- negative testcases
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index bde29e38a9..c4e827caec 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2463,74 +2463,43 @@ deallocate ab_q6;
 insert into ab values (1,2);
 explain (analyze, costs off, summary off, timing off)
 update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
-                                     QUERY PLAN                                      
--------------------------------------------------------------------------------------
+                                        QUERY PLAN                                         
+-------------------------------------------------------------------------------------------
  Update on ab_a1 (actual rows=0 loops=1)
    Update on ab_a1_b1 ab_a1_1
    Update on ab_a1_b2 ab_a1_2
    Update on ab_a1_b3 ab_a1_3
-   ->  Nested Loop (actual rows=0 loops=1)
-         ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-         ->  Materialize (actual rows=0 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_a1_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
    ->  Nested Loop (actual rows=1 loops=1)
          ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+               ->  Bitmap Heap Scan on ab_a1_b1 ab_a1_1 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-         ->  Materialize (actual rows=1 loops=1)
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
-   ->  Nested Loop (actual rows=0 loops=1)
-         ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-         ->  Materialize (actual rows=0 loops=1)
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
-(65 rows)
+         ->  Materialize (actual rows=1 loops=1)
+               ->  Append (actual rows=1 loops=1)
+                     ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+                           Recheck Cond: (a = 1)
+                           ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
+                                 Index Cond: (a = 1)
+                     ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
+                           Recheck Cond: (a = 1)
+                           Heap Blocks: exact=1
+                           ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
+                                 Index Cond: (a = 1)
+                     ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
+                           Recheck Cond: (a = 1)
+                           ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
+                                 Index Cond: (a = 1)
+(34 rows)
 
 table ab;
  a | b 
@@ -2551,29 +2520,12 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);
    Update on ab_a1_b3 ab_a1_3
    InitPlan 1 (returns $0)
      ->  Result (actual rows=1 loops=1)
-   ->  Nested Loop (actual rows=1 loops=1)
-         ->  Seq Scan on ab_a1_b1 ab_a1_1 (actual rows=1 loops=1)
-         ->  Materialize (actual rows=1 loops=1)
-               ->  Append (actual rows=1 loops=1)
-                     ->  Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
-                           Filter: (b = $0)
-                     ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = $0)
-                     ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = $0)
-   ->  Nested Loop (actual rows=1 loops=1)
-         ->  Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
-         ->  Materialize (actual rows=1 loops=1)
-               ->  Append (actual rows=1 loops=1)
-                     ->  Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
-                           Filter: (b = $0)
-                     ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = $0)
-                     ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = $0)
-   ->  Nested Loop (actual rows=1 loops=1)
-         ->  Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=1 loops=1)
-         ->  Materialize (actual rows=1 loops=1)
+   ->  Nested Loop (actual rows=3 loops=1)
+         ->  Append (actual rows=3 loops=1)
+               ->  Seq Scan on ab_a1_b1 ab_a1_1 (actual rows=1 loops=1)
+               ->  Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
+               ->  Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=1 loops=1)
+         ->  Materialize (actual rows=1 loops=3)
                ->  Append (actual rows=1 loops=1)
                      ->  Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
                            Filter: (b = $0)
@@ -2581,7 +2533,7 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);
                            Filter: (b = $0)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
                            Filter: (b = $0)
-(36 rows)
+(19 rows)
 
 select tableoid::regclass, * from ab;
  tableoid | a | b 
@@ -3420,28 +3372,30 @@ explain (costs off) select * from pp_lp where a = 1;
 (5 rows)
 
 explain (costs off) update pp_lp set value = 10 where a = 1;
-            QUERY PLAN            
-----------------------------------
+               QUERY PLAN               
+----------------------------------------
  Update on pp_lp
    Update on pp_lp1 pp_lp_1
    Update on pp_lp2 pp_lp_2
-   ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-   ->  Seq Scan on pp_lp2 pp_lp_2
-         Filter: (a = 1)
-(7 rows)
+   ->  Append
+         ->  Seq Scan on pp_lp1 pp_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on pp_lp2 pp_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 explain (costs off) delete from pp_lp where a = 1;
-            QUERY PLAN            
-----------------------------------
+               QUERY PLAN               
+----------------------------------------
  Delete on pp_lp
    Delete on pp_lp1 pp_lp_1
    Delete on pp_lp2 pp_lp_2
-   ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-   ->  Seq Scan on pp_lp2 pp_lp_2
-         Filter: (a = 1)
-(7 rows)
+   ->  Append
+         ->  Seq Scan on pp_lp1 pp_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on pp_lp2 pp_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 set constraint_exclusion = 'off'; -- this should not affect the result.
 explain (costs off) select * from pp_lp where a = 1;
@@ -3455,28 +3409,30 @@ explain (costs off) select * from pp_lp where a = 1;
 (5 rows)
 
 explain (costs off) update pp_lp set value = 10 where a = 1;
-            QUERY PLAN            
-----------------------------------
+               QUERY PLAN               
+----------------------------------------
  Update on pp_lp
    Update on pp_lp1 pp_lp_1
    Update on pp_lp2 pp_lp_2
-   ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-   ->  Seq Scan on pp_lp2 pp_lp_2
-         Filter: (a = 1)
-(7 rows)
+   ->  Append
+         ->  Seq Scan on pp_lp1 pp_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on pp_lp2 pp_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 explain (costs off) delete from pp_lp where a = 1;
-            QUERY PLAN            
-----------------------------------
+               QUERY PLAN               
+----------------------------------------
  Delete on pp_lp
    Delete on pp_lp1 pp_lp_1
    Delete on pp_lp2 pp_lp_2
-   ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-   ->  Seq Scan on pp_lp2 pp_lp_2
-         Filter: (a = 1)
-(7 rows)
+   ->  Append
+         ->  Seq Scan on pp_lp1 pp_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on pp_lp2 pp_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 drop table pp_lp;
 -- Ensure enable_partition_prune does not affect non-partitioned tables.
@@ -3500,28 +3456,31 @@ explain (costs off) select * from inh_lp where a = 1;
 (5 rows)
 
 explain (costs off) update inh_lp set value = 10 where a = 1;
-             QUERY PLAN             
-------------------------------------
+                   QUERY PLAN                   
+------------------------------------------------
  Update on inh_lp
-   Update on inh_lp
-   Update on inh_lp1 inh_lp_1
-   ->  Seq Scan on inh_lp
-         Filter: (a = 1)
-   ->  Seq Scan on inh_lp1 inh_lp_1
-         Filter: (a = 1)
-(7 rows)
+   Update on inh_lp inh_lp_1
+   Update on inh_lp1 inh_lp_2
+   ->  Result
+         ->  Append
+               ->  Seq Scan on inh_lp inh_lp_1
+                     Filter: (a = 1)
+               ->  Seq Scan on inh_lp1 inh_lp_2
+                     Filter: (a = 1)
+(9 rows)
 
 explain (costs off) delete from inh_lp where a = 1;
-             QUERY PLAN             
-------------------------------------
+                QUERY PLAN                
+------------------------------------------
  Delete on inh_lp
-   Delete on inh_lp
-   Delete on inh_lp1 inh_lp_1
-   ->  Seq Scan on inh_lp
-         Filter: (a = 1)
-   ->  Seq Scan on inh_lp1 inh_lp_1
-         Filter: (a = 1)
-(7 rows)
+   Delete on inh_lp inh_lp_1
+   Delete on inh_lp1 inh_lp_2
+   ->  Append
+         ->  Seq Scan on inh_lp inh_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on inh_lp1 inh_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 -- Ensure we don't exclude normal relations when we only expect to exclude
 -- inheritance children
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 9506aaef82..b02a682471 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -1632,19 +1632,21 @@ EXPLAIN (COSTS OFF) EXECUTE p2(2);
 --
 SET SESSION AUTHORIZATION regress_rls_bob;
 EXPLAIN (COSTS OFF) UPDATE t1 SET b = b || b WHERE f_leak(b);
-                  QUERY PLAN                   
------------------------------------------------
+                        QUERY PLAN                         
+-----------------------------------------------------------
  Update on t1
-   Update on t1
-   Update on t2 t1_1
-   Update on t3 t1_2
-   ->  Seq Scan on t1
-         Filter: (((a % 2) = 0) AND f_leak(b))
-   ->  Seq Scan on t2 t1_1
-         Filter: (((a % 2) = 0) AND f_leak(b))
-   ->  Seq Scan on t3 t1_2
-         Filter: (((a % 2) = 0) AND f_leak(b))
-(10 rows)
+   Update on t1 t1_1
+   Update on t2 t1_2
+   Update on t3 t1_3
+   ->  Result
+         ->  Append
+               ->  Seq Scan on t1 t1_1
+                     Filter: (((a % 2) = 0) AND f_leak(b))
+               ->  Seq Scan on t2 t1_2
+                     Filter: (((a % 2) = 0) AND f_leak(b))
+               ->  Seq Scan on t3 t1_3
+                     Filter: (((a % 2) = 0) AND f_leak(b))
+(12 rows)
 
 UPDATE t1 SET b = b || b WHERE f_leak(b);
 NOTICE:  f_leak => bbb
@@ -1722,31 +1724,27 @@ NOTICE:  f_leak => cde
 NOTICE:  f_leak => yyyyyy
 EXPLAIN (COSTS OFF) UPDATE t1 SET b=t1.b FROM t2
 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
-                           QUERY PLAN                            
------------------------------------------------------------------
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
  Update on t1
-   Update on t1
-   Update on t2 t1_1
-   Update on t3 t1_2
-   ->  Nested Loop
-         ->  Seq Scan on t1
-               Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
-         ->  Seq Scan on t2
-               Filter: ((a = 3) AND ((a % 2) = 1) AND f_leak(b))
-   ->  Nested Loop
-         ->  Seq Scan on t2 t1_1
-               Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
-         ->  Seq Scan on t2
-               Filter: ((a = 3) AND ((a % 2) = 1) AND f_leak(b))
+   Update on t1 t1_1
+   Update on t2 t1_2
+   Update on t3 t1_3
    ->  Nested Loop
-         ->  Seq Scan on t3 t1_2
-               Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
          ->  Seq Scan on t2
                Filter: ((a = 3) AND ((a % 2) = 1) AND f_leak(b))
-(19 rows)
+         ->  Append
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
+               ->  Seq Scan on t2 t1_2
+                     Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
+               ->  Seq Scan on t3 t1_3
+                     Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
+(14 rows)
 
 UPDATE t1 SET b=t1.b FROM t2
 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+NOTICE:  f_leak => cde
 EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t1
 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
                               QUERY PLAN                               
@@ -1795,46 +1793,30 @@ NOTICE:  f_leak => cde
 EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
 WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
 AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
-                              QUERY PLAN                               
------------------------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Update on t1 t1_1
-   Update on t1 t1_1
-   Update on t2 t1_1_1
-   Update on t3 t1_1_2
+   Update on t1 t1_1_1
+   Update on t2 t1_1_2
+   Update on t3 t1_1_3
    ->  Nested Loop
          Join Filter: (t1_1.b = t1_2.b)
-         ->  Seq Scan on t1 t1_1
-               Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-         ->  Append
-               ->  Seq Scan on t1 t1_2_1
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t2 t1_2_2
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t3 t1_2_3
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-   ->  Nested Loop
-         Join Filter: (t1_1_1.b = t1_2.b)
-         ->  Seq Scan on t2 t1_1_1
-               Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
          ->  Append
-               ->  Seq Scan on t1 t1_2_1
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t2 t1_2_2
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t3 t1_2_3
+               ->  Seq Scan on t1 t1_1_1
                      Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-   ->  Nested Loop
-         Join Filter: (t1_1_2.b = t1_2.b)
-         ->  Seq Scan on t3 t1_1_2
-               Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-         ->  Append
-               ->  Seq Scan on t1 t1_2_1
+               ->  Seq Scan on t2 t1_1_2
                      Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t2 t1_2_2
+               ->  Seq Scan on t3 t1_1_3
                      Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t3 t1_2_3
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-(37 rows)
+         ->  Materialize
+               ->  Append
+                     ->  Seq Scan on t1 t1_2_1
+                           Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
+                     ->  Seq Scan on t2 t1_2_2
+                           Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
+                     ->  Seq Scan on t3 t1_2_3
+                           Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
+(21 rows)
 
 UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
 WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
@@ -1842,8 +1824,6 @@ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
 NOTICE:  f_leak => daddad_updt
 NOTICE:  f_leak => daddad_updt
 NOTICE:  f_leak => defdef
-NOTICE:  f_leak => defdef
-NOTICE:  f_leak => daddad_updt
 NOTICE:  f_leak => defdef
  id  | a |      b      | id  | a |      b      |        t1_1         |        t1_2         
 -----+---+-------------+-----+---+-------------+---------------------+---------------------
@@ -1880,19 +1860,20 @@ EXPLAIN (COSTS OFF) DELETE FROM only t1 WHERE f_leak(b);
 (3 rows)
 
 EXPLAIN (COSTS OFF) DELETE FROM t1 WHERE f_leak(b);
-                  QUERY PLAN                   
------------------------------------------------
+                     QUERY PLAN                      
+-----------------------------------------------------
  Delete on t1
-   Delete on t1
-   Delete on t2 t1_1
-   Delete on t3 t1_2
-   ->  Seq Scan on t1
-         Filter: (((a % 2) = 0) AND f_leak(b))
-   ->  Seq Scan on t2 t1_1
-         Filter: (((a % 2) = 0) AND f_leak(b))
-   ->  Seq Scan on t3 t1_2
-         Filter: (((a % 2) = 0) AND f_leak(b))
-(10 rows)
+   Delete on t1 t1_1
+   Delete on t2 t1_2
+   Delete on t3 t1_3
+   ->  Append
+         ->  Seq Scan on t1 t1_1
+               Filter: (((a % 2) = 0) AND f_leak(b))
+         ->  Seq Scan on t2 t1_2
+               Filter: (((a % 2) = 0) AND f_leak(b))
+         ->  Seq Scan on t3 t1_3
+               Filter: (((a % 2) = 0) AND f_leak(b))
+(11 rows)
 
 DELETE FROM only t1 WHERE f_leak(b) RETURNING tableoid::regclass, *, t1;
 NOTICE:  f_leak => bbbbbb_updt
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 770eab38b5..cdff914b93 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -1607,26 +1607,21 @@ UPDATE rw_view1 SET a = a + 1000 FROM other_tbl_parent WHERE a = id;
                                QUERY PLAN                                
 -------------------------------------------------------------------------
  Update on base_tbl_parent
-   Update on base_tbl_parent
-   Update on base_tbl_child base_tbl_parent_1
-   ->  Hash Join
-         Hash Cond: (other_tbl_parent.id = base_tbl_parent.a)
-         ->  Append
-               ->  Seq Scan on other_tbl_parent other_tbl_parent_1
-               ->  Seq Scan on other_tbl_child other_tbl_parent_2
-         ->  Hash
-               ->  Seq Scan on base_tbl_parent
+   Update on base_tbl_parent base_tbl_parent_1
+   Update on base_tbl_child base_tbl_parent_2
    ->  Merge Join
-         Merge Cond: (base_tbl_parent_1.a = other_tbl_parent.id)
+         Merge Cond: (base_tbl_parent.a = other_tbl_parent.id)
          ->  Sort
-               Sort Key: base_tbl_parent_1.a
-               ->  Seq Scan on base_tbl_child base_tbl_parent_1
+               Sort Key: base_tbl_parent.a
+               ->  Append
+                     ->  Seq Scan on base_tbl_parent base_tbl_parent_1
+                     ->  Seq Scan on base_tbl_child base_tbl_parent_2
          ->  Sort
                Sort Key: other_tbl_parent.id
                ->  Append
                      ->  Seq Scan on other_tbl_parent other_tbl_parent_1
                      ->  Seq Scan on other_tbl_child other_tbl_parent_2
-(20 rows)
+(15 rows)
 
 UPDATE rw_view1 SET a = a + 1000 FROM other_tbl_parent WHERE a = id;
 SELECT * FROM ONLY base_tbl_parent ORDER BY a;
@@ -2332,36 +2327,39 @@ SELECT * FROM v1 WHERE a=8;
 
 EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                             QUERY PLAN                                              
+-----------------------------------------------------------------------------------------------------
  Update on public.t1
-   Update on public.t1
-   Update on public.t11 t1_1
-   Update on public.t12 t1_2
-   Update on public.t111 t1_3
-   ->  Index Scan using t1_a_idx on public.t1
-         Output: 100, t1.ctid
-         Index Cond: ((t1.a > 5) AND (t1.a < 7))
-         Filter: ((t1.a <> 6) AND (SubPlan 1) AND snoop(t1.a) AND leakproof(t1.a))
-         SubPlan 1
-           ->  Append
-                 ->  Seq Scan on public.t12 t12_1
-                       Filter: (t12_1.a = t1.a)
-                 ->  Seq Scan on public.t111 t12_2
-                       Filter: (t12_2.a = t1.a)
-   ->  Index Scan using t11_a_idx on public.t11 t1_1
-         Output: 100, t1_1.ctid
-         Index Cond: ((t1_1.a > 5) AND (t1_1.a < 7))
-         Filter: ((t1_1.a <> 6) AND (SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
-   ->  Index Scan using t12_a_idx on public.t12 t1_2
-         Output: 100, t1_2.ctid
-         Index Cond: ((t1_2.a > 5) AND (t1_2.a < 7))
-         Filter: ((t1_2.a <> 6) AND (SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
-   ->  Index Scan using t111_a_idx on public.t111 t1_3
-         Output: 100, t1_3.ctid
-         Index Cond: ((t1_3.a > 5) AND (t1_3.a < 7))
-         Filter: ((t1_3.a <> 6) AND (SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
-(27 rows)
+   Update on public.t1 t1_1
+   Update on public.t11 t1_2
+   Update on public.t12 t1_3
+   Update on public.t111 t1_4
+   ->  Result
+         Output: 100, t1.tableoid, t1.ctid
+         ->  Append
+               ->  Index Scan using t1_a_idx on public.t1 t1_1
+                     Output: t1_1.tableoid, t1_1.ctid
+                     Index Cond: ((t1_1.a > 5) AND (t1_1.a < 7))
+                     Filter: ((t1_1.a <> 6) AND (SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
+                     SubPlan 1
+                       ->  Append
+                             ->  Seq Scan on public.t12 t12_1
+                                   Filter: (t12_1.a = t1_1.a)
+                             ->  Seq Scan on public.t111 t12_2
+                                   Filter: (t12_2.a = t1_1.a)
+               ->  Index Scan using t11_a_idx on public.t11 t1_2
+                     Output: t1_2.tableoid, t1_2.ctid
+                     Index Cond: ((t1_2.a > 5) AND (t1_2.a < 7))
+                     Filter: ((t1_2.a <> 6) AND (SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
+               ->  Index Scan using t12_a_idx on public.t12 t1_3
+                     Output: t1_3.tableoid, t1_3.ctid
+                     Index Cond: ((t1_3.a > 5) AND (t1_3.a < 7))
+                     Filter: ((t1_3.a <> 6) AND (SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
+               ->  Index Scan using t111_a_idx on public.t111 t1_4
+                     Output: t1_4.tableoid, t1_4.ctid
+                     Index Cond: ((t1_4.a > 5) AND (t1_4.a < 7))
+                     Filter: ((t1_4.a <> 6) AND (SubPlan 1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
+(30 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2376,36 +2374,39 @@ SELECT * FROM t1 WHERE a=100; -- Nothing should have been changed to 100
 
 EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
-                              QUERY PLAN                               
------------------------------------------------------------------------
+                                    QUERY PLAN                                     
+-----------------------------------------------------------------------------------
  Update on public.t1
-   Update on public.t1
-   Update on public.t11 t1_1
-   Update on public.t12 t1_2
-   Update on public.t111 t1_3
-   ->  Index Scan using t1_a_idx on public.t1
-         Output: (t1.a + 1), t1.ctid
-         Index Cond: ((t1.a > 5) AND (t1.a = 8))
-         Filter: ((SubPlan 1) AND snoop(t1.a) AND leakproof(t1.a))
-         SubPlan 1
-           ->  Append
-                 ->  Seq Scan on public.t12 t12_1
-                       Filter: (t12_1.a = t1.a)
-                 ->  Seq Scan on public.t111 t12_2
-                       Filter: (t12_2.a = t1.a)
-   ->  Index Scan using t11_a_idx on public.t11 t1_1
-         Output: (t1_1.a + 1), t1_1.ctid
-         Index Cond: ((t1_1.a > 5) AND (t1_1.a = 8))
-         Filter: ((SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
-   ->  Index Scan using t12_a_idx on public.t12 t1_2
-         Output: (t1_2.a + 1), t1_2.ctid
-         Index Cond: ((t1_2.a > 5) AND (t1_2.a = 8))
-         Filter: ((SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
-   ->  Index Scan using t111_a_idx on public.t111 t1_3
-         Output: (t1_3.a + 1), t1_3.ctid
-         Index Cond: ((t1_3.a > 5) AND (t1_3.a = 8))
-         Filter: ((SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
-(27 rows)
+   Update on public.t1 t1_1
+   Update on public.t11 t1_2
+   Update on public.t12 t1_3
+   Update on public.t111 t1_4
+   ->  Result
+         Output: (t1.a + 1), t1.tableoid, t1.ctid
+         ->  Append
+               ->  Index Scan using t1_a_idx on public.t1 t1_1
+                     Output: t1_1.a, t1_1.tableoid, t1_1.ctid
+                     Index Cond: ((t1_1.a > 5) AND (t1_1.a = 8))
+                     Filter: ((SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
+                     SubPlan 1
+                       ->  Append
+                             ->  Seq Scan on public.t12 t12_1
+                                   Filter: (t12_1.a = t1_1.a)
+                             ->  Seq Scan on public.t111 t12_2
+                                   Filter: (t12_2.a = t1_1.a)
+               ->  Index Scan using t11_a_idx on public.t11 t1_2
+                     Output: t1_2.a, t1_2.tableoid, t1_2.ctid
+                     Index Cond: ((t1_2.a > 5) AND (t1_2.a = 8))
+                     Filter: ((SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
+               ->  Index Scan using t12_a_idx on public.t12 t1_3
+                     Output: t1_3.a, t1_3.tableoid, t1_3.ctid
+                     Index Cond: ((t1_3.a > 5) AND (t1_3.a = 8))
+                     Filter: ((SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
+               ->  Index Scan using t111_a_idx on public.t111 t1_4
+                     Output: t1_4.a, t1_4.tableoid, t1_4.ctid
+                     Index Cond: ((t1_4.a > 5) AND (t1_4.a = 8))
+                     Filter: ((SubPlan 1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
+(30 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out
index dece036069..dc34ac67b3 100644
--- a/src/test/regress/expected/update.out
+++ b/src/test/regress/expected/update.out
@@ -308,8 +308,8 @@ ALTER TABLE part_b_10_b_20 ATTACH PARTITION part_c_1_100 FOR VALUES FROM (1) TO
 
 -- The order of subplans should be in bound order
 EXPLAIN (costs off) UPDATE range_parted set c = c - 50 WHERE c > 97;
-                   QUERY PLAN                    
--------------------------------------------------
+                      QUERY PLAN                       
+-------------------------------------------------------
  Update on range_parted
    Update on part_a_1_a_10 range_parted_1
    Update on part_a_10_a_20 range_parted_2
@@ -318,21 +318,22 @@ EXPLAIN (costs off) UPDATE range_parted set c = c - 50 WHERE c > 97;
    Update on part_d_1_15 range_parted_5
    Update on part_d_15_20 range_parted_6
    Update on part_b_20_b_30 range_parted_7
-   ->  Seq Scan on part_a_1_a_10 range_parted_1
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_a_10_a_20 range_parted_2
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_b_1_b_10 range_parted_3
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_c_1_100 range_parted_4
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_d_1_15 range_parted_5
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_d_15_20 range_parted_6
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_b_20_b_30 range_parted_7
-         Filter: (c > '97'::numeric)
-(22 rows)
+   ->  Append
+         ->  Seq Scan on part_a_1_a_10 range_parted_1
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_a_10_a_20 range_parted_2
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_b_1_b_10 range_parted_3
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_c_1_100 range_parted_4
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_d_1_15 range_parted_5
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_d_15_20 range_parted_6
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_b_20_b_30 range_parted_7
+               Filter: (c > '97'::numeric)
+(23 rows)
 
 -- fail, row movement happens only within the partition subtree.
 UPDATE part_c_100_200 set c = c - 20, d = c WHERE c = 105;
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 9a6b716ddc..0affacc191 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2906,47 +2906,35 @@ SELECT * FROM parent;
 EXPLAIN (VERBOSE, COSTS OFF)
 WITH wcte AS ( INSERT INTO int8_tbl VALUES ( 42, 47 ) RETURNING q2 )
 DELETE FROM a USING wcte WHERE aa = q2;
-                     QUERY PLAN                     
-----------------------------------------------------
+                         QUERY PLAN                         
+------------------------------------------------------------
  Delete on public.a
-   Delete on public.a
-   Delete on public.b a_1
-   Delete on public.c a_2
-   Delete on public.d a_3
+   Delete on public.a a_1
+   Delete on public.b a_2
+   Delete on public.c a_3
+   Delete on public.d a_4
    CTE wcte
      ->  Insert on public.int8_tbl
            Output: int8_tbl.q2
            ->  Result
                  Output: '42'::bigint, '47'::bigint
-   ->  Nested Loop
-         Output: a.ctid, wcte.*
-         Join Filter: (a.aa = wcte.q2)
-         ->  Seq Scan on public.a
-               Output: a.ctid, a.aa
-         ->  CTE Scan on wcte
+   ->  Hash Join
+         Output: wcte.*, a.tableoid, a.ctid
+         Hash Cond: (a.aa = wcte.q2)
+         ->  Append
+               ->  Seq Scan on public.a a_1
+                     Output: a_1.aa, a_1.tableoid, a_1.ctid
+               ->  Seq Scan on public.b a_2
+                     Output: a_2.aa, a_2.tableoid, a_2.ctid
+               ->  Seq Scan on public.c a_3
+                     Output: a_3.aa, a_3.tableoid, a_3.ctid
+               ->  Seq Scan on public.d a_4
+                     Output: a_4.aa, a_4.tableoid, a_4.ctid
+         ->  Hash
                Output: wcte.*, wcte.q2
-   ->  Nested Loop
-         Output: a_1.ctid, wcte.*
-         Join Filter: (a_1.aa = wcte.q2)
-         ->  Seq Scan on public.b a_1
-               Output: a_1.ctid, a_1.aa
-         ->  CTE Scan on wcte
-               Output: wcte.*, wcte.q2
-   ->  Nested Loop
-         Output: a_2.ctid, wcte.*
-         Join Filter: (a_2.aa = wcte.q2)
-         ->  Seq Scan on public.c a_2
-               Output: a_2.ctid, a_2.aa
-         ->  CTE Scan on wcte
-               Output: wcte.*, wcte.q2
-   ->  Nested Loop
-         Output: a_3.ctid, wcte.*
-         Join Filter: (a_3.aa = wcte.q2)
-         ->  Seq Scan on public.d a_3
-               Output: a_3.ctid, a_3.aa
-         ->  CTE Scan on wcte
-               Output: wcte.*, wcte.q2
-(38 rows)
+               ->  CTE Scan on wcte
+                     Output: wcte.*, wcte.q2
+(26 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
