diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 6faf499f9a..cff23b0211 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -1867,6 +1867,7 @@ deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
  * 'foreignrel' is the RelOptInfo for the target relation or the join relation
  *		containing all base relations in the query
  * 'targetlist' is the tlist of the underlying foreign-scan plan node
+ *		(note that this only contains new-value expressions and junk attrs)
  * 'targetAttrs' is the target columns of the UPDATE
  * 'remote_conds' is the qual clauses that must be evaluated remotely
  * '*params_list' is an output list of exprs that will become remote Params
@@ -1888,8 +1889,8 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 	deparse_expr_cxt context;
 	int			nestlevel;
 	bool		first;
-	ListCell   *lc;
 	RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
+	ListCell   *lc, *lc2;
 
 	/* Set up context struct for recursion */
 	context.root = root;
@@ -1908,14 +1909,13 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 	nestlevel = set_transmission_modes();
 
 	first = true;
-	foreach(lc, targetAttrs)
+	forboth(lc, targetlist, lc2, targetAttrs)
 	{
-		int			attnum = lfirst_int(lc);
-		TargetEntry *tle = get_tle_by_resno(targetlist, attnum);
+		TargetEntry *tle = lfirst_node(TargetEntry, lc);
+		int attnum = lfirst_int(lc2);
 
-		if (!tle)
-			elog(ERROR, "attribute number %d not found in UPDATE targetlist",
-				 attnum);
+		/* update's new-value expressions shouldn't be resjunk */
+		Assert(!tle->resjunk);
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 0649b6b81c..b46e7e623f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -5503,13 +5503,13 @@ UPDATE ft2 AS target SET (c2, c7) = (
         FROM ft2 AS src
         WHERE target.c1 = src.c1
 ) WHERE c1 > 1100;
-                                                                    QUERY PLAN                                                                     
----------------------------------------------------------------------------------------------------------------------------------------------------
+                                                      QUERY PLAN                                                       
+-----------------------------------------------------------------------------------------------------------------------
  Update on public.ft2 target
    Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c7 = $3 WHERE ctid = $1
    ->  Foreign Scan on public.ft2 target
-         Output: target.c1, $1, NULL::integer, target.c3, target.c4, target.c5, target.c6, $2, target.c8, (SubPlan 1 (returns $1,$2)), target.ctid
-         Remote SQL: SELECT "C 1", c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1100)) FOR UPDATE
+         Output: $1, $2, (SubPlan 1 (returns $1,$2)), target.ctid, target.*
+         Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1100)) FOR UPDATE
          SubPlan 1 (returns $1,$2)
            ->  Foreign Scan on public.ft2 src
                  Output: (src.c2 * 10), src.c7
@@ -5539,9 +5539,9 @@ UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING *;
    Output: c1, c2, c3, c4, c5, c6, c7, c8
    Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8
    ->  Foreign Scan on public.ft2
-         Output: c1, c2, NULL::integer, 'bar'::text, c4, c5, c6, c7, c8, ctid
+         Output: 'bar'::text, ctid, ft2.*
          Filter: (postgres_fdw_abs(ft2.c1) > 2000)
-         Remote SQL: SELECT "C 1", c2, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" FOR UPDATE
+         Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" FOR UPDATE
 (7 rows)
 
 UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING *;
@@ -5570,11 +5570,11 @@ UPDATE ft2 SET c3 = 'baz'
    Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3
    Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8
    ->  Nested Loop
-         Output: ft2.c1, ft2.c2, NULL::integer, 'baz'::text, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.ctid, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3
+         Output: 'baz'::text, ft2.ctid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3
          Join Filter: (ft2.c2 === ft4.c1)
          ->  Foreign Scan on public.ft2
-               Output: ft2.c1, ft2.c2, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft2.ctid
-               Remote SQL: SELECT "C 1", c2, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE
+               Output: ft2.ctid, ft2.*, ft2.c2
+               Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE
          ->  Foreign Scan
                Output: ft4.*, ft4.c1, ft4.c2, ft4.c3, ft5.*, ft5.c1, ft5.c2, ft5.c3
                Relations: (public.ft4) INNER JOIN (public.ft5)
@@ -6266,7 +6266,7 @@ UPDATE rw_view SET b = b + 5;
  Update on public.foreign_tbl
    Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
    ->  Foreign Scan on public.foreign_tbl
-         Output: foreign_tbl.a, (foreign_tbl.b + 5), foreign_tbl.ctid
+         Output: (foreign_tbl.b + 5), foreign_tbl.ctid, foreign_tbl.*
          Remote SQL: SELECT a, b, ctid FROM public.base_tbl WHERE ((a < b)) FOR UPDATE
 (5 rows)
 
@@ -6280,7 +6280,7 @@ UPDATE rw_view SET b = b + 15;
  Update on public.foreign_tbl
    Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
    ->  Foreign Scan on public.foreign_tbl
-         Output: foreign_tbl.a, (foreign_tbl.b + 15), foreign_tbl.ctid
+         Output: (foreign_tbl.b + 15), foreign_tbl.ctid, foreign_tbl.*
          Remote SQL: SELECT a, b, ctid FROM public.base_tbl WHERE ((a < b)) FOR UPDATE
 (5 rows)
 
@@ -6354,7 +6354,7 @@ UPDATE rw_view SET b = b + 5;
    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.a, (parent_tbl_1.b + 5), parent_tbl_1.ctid
+         Output: (parent_tbl_1.b + 5), parent_tbl_1.ctid, parent_tbl_1.*
          Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE
 (6 rows)
 
@@ -6369,7 +6369,7 @@ UPDATE rw_view SET b = b + 15;
    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.a, (parent_tbl_1.b + 15), parent_tbl_1.ctid
+         Output: (parent_tbl_1.b + 15), parent_tbl_1.ctid, parent_tbl_1.*
          Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE
 (6 rows)
 
@@ -6686,7 +6686,7 @@ UPDATE rem1 set f1 = 10;          -- all columns should be transmitted
  Update on public.rem1
    Remote SQL: UPDATE public.loc1 SET f1 = $2, f2 = $3 WHERE ctid = $1
    ->  Foreign Scan on public.rem1
-         Output: 10, f2, ctid, rem1.*
+         Output: 10, ctid, rem1.*
          Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE
 (5 rows)
 
@@ -6919,7 +6919,7 @@ UPDATE rem1 set f2 = '';          -- can't be pushed down
  Update on public.rem1
    Remote SQL: UPDATE public.loc1 SET f1 = $2, f2 = $3 WHERE ctid = $1
    ->  Foreign Scan on public.rem1
-         Output: f1, ''::text, ctid, rem1.*
+         Output: ''::text, ctid, rem1.*
          Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE
 (5 rows)
 
@@ -6943,7 +6943,7 @@ UPDATE rem1 set f2 = '';          -- can't be pushed down
  Update on public.rem1
    Remote SQL: UPDATE public.loc1 SET f2 = $2 WHERE ctid = $1 RETURNING f1, f2
    ->  Foreign Scan on public.rem1
-         Output: f1, ''::text, ctid, rem1.*
+         Output: ''::text, ctid, rem1.*
          Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE
 (5 rows)
 
@@ -7253,18 +7253,18 @@ 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
      Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1
    ->  Hash Join
-         Output: bar.f1, (bar.f2 + 100), bar.ctid, foo.ctid, foo.*, foo.tableoid
+         Output: (bar.f2 + 100), bar.ctid, foo.ctid, foo.*, foo.tableoid
          Inner Unique: true
          Hash Cond: (bar.f1 = foo.f1)
          ->  Seq Scan on public.bar
-               Output: bar.f1, bar.f2, bar.ctid
+               Output: bar.f2, bar.ctid, bar.f1
          ->  Hash
                Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                ->  HashAggregate
@@ -7277,11 +7277,11 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                                  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.f1, (bar_1.f2 + 100), bar_1.f3, bar_1.ctid, foo.ctid, foo.*, foo.tableoid
+         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.f1, bar_1.f2, bar_1.f3, bar_1.ctid
+               Output: bar_1.f2, bar_1.ctid, bar_1.*, bar_1.f1
                Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
          ->  Hash
                Output: foo.ctid, foo.f1, foo.*, foo.tableoid
@@ -7321,7 +7321,7 @@ where bar.f1 = ss.f1;
    Foreign Update on public.bar2 bar_1
      Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1
    ->  Hash Join
-         Output: bar.f1, (bar.f2 + 100), bar.ctid, (ROW(foo.f1))
+         Output: (bar.f2 + 100), bar.ctid, (ROW(foo.f1))
          Hash Cond: (foo.f1 = bar.f1)
          ->  Append
                ->  Seq Scan on public.foo
@@ -7335,17 +7335,17 @@ where bar.f1 = ss.f1;
                      Output: ROW((foo_3.f1 + 3)), (foo_3.f1 + 3)
                      Remote SQL: SELECT f1 FROM public.loct1
          ->  Hash
-               Output: bar.f1, bar.f2, bar.ctid
+               Output: bar.f2, bar.ctid, bar.f1
                ->  Seq Scan on public.bar
-                     Output: bar.f1, bar.f2, bar.ctid
+                     Output: bar.f2, bar.ctid, bar.f1
    ->  Merge Join
-         Output: bar_1.f1, (bar_1.f2 + 100), bar_1.f3, bar_1.ctid, (ROW(foo.f1))
+         Output: (bar_1.f2 + 100), bar_1.ctid, bar_1.*, (ROW(foo.f1))
          Merge Cond: (bar_1.f1 = foo.f1)
          ->  Sort
-               Output: bar_1.f1, bar_1.f2, bar_1.f3, bar_1.ctid
+               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.f1, bar_1.f2, bar_1.f3, bar_1.ctid
+                     Output: bar_1.f2, bar_1.ctid, bar_1.*, bar_1.f1
                      Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
          ->  Sort
                Output: (ROW(foo.f1)), foo.f1
@@ -7519,7 +7519,7 @@ update bar set f2 = f2 + 100 returning *;
    Update on public.bar
    Foreign Update on public.bar2 bar_1
    ->  Seq Scan on public.bar
-         Output: bar.f1, (bar.f2 + 100), bar.ctid
+         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)
@@ -7551,9 +7551,9 @@ update bar set f2 = f2 + 100;
    Foreign Update on public.bar2 bar_1
      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.f1, (bar.f2 + 100), bar.ctid
+         Output: (bar.f2 + 100), bar.ctid
    ->  Foreign Scan on public.bar2 bar_1
-         Output: bar_1.f1, (bar_1.f2 + 100), bar_1.f3, bar_1.ctid, 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)
 
@@ -7622,10 +7622,10 @@ update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a re
    Update on public.parent
    Foreign Update on public.remt1 parent_1
    ->  Nested Loop
-         Output: parent.a, (parent.b || remt2.b), parent.ctid, remt2.*, remt2.a, remt2.b
+         Output: (parent.b || remt2.b), parent.ctid, remt2.*, remt2.a, remt2.b
          Join Filter: (parent.a = remt2.a)
          ->  Seq Scan on public.parent
-               Output: parent.a, parent.b, parent.ctid
+               Output: parent.b, parent.ctid, parent.a
          ->  Foreign Scan on public.remt2
                Output: remt2.b, remt2.*, remt2.a
                Remote SQL: SELECT a, b FROM public.loct2
@@ -7880,7 +7880,7 @@ update utrtest set a = 1 where a = 1 or a = 2 returning *;
    ->  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.b, utrtest_2.ctid
+         Output: 1, utrtest_2.ctid
          Filter: ((utrtest_2.a = 1) OR (utrtest_2.a = 2))
 (9 rows)
 
@@ -7896,13 +7896,13 @@ 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.b, utrtest_1.ctid
+         Output: 1, utrtest_1.ctid
          Filter: (utrtest_1.a = 2)
 (6 rows)
 
@@ -7932,7 +7932,7 @@ update utrtest set a = 1 returning *;
    ->  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.b, utrtest_2.ctid
+         Output: 1, utrtest_2.ctid
 (8 rows)
 
 update utrtest set a = 1 returning *;
@@ -7956,20 +7956,20 @@ update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
      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.b, utrtest_1.ctid, "*VALUES*".*, "*VALUES*".column1
+         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.b, utrtest_1.ctid, utrtest_1.a
+               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.b, utrtest_2.ctid, "*VALUES*".*, "*VALUES*".column1
+         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.b, utrtest_2.ctid, utrtest_2.a
+               Output: utrtest_2.ctid, utrtest_2.a
          ->  Hash
                Output: "*VALUES*".*, "*VALUES*".column1
                ->  Values Scan on "*VALUES*"
@@ -7977,12 +7977,7 @@ update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
 (24 rows)
 
 update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
- a |  b  | x 
----+-----+---
- 1 | foo | 1
- 1 | qux | 2
-(2 rows)
-
+ERROR:  invalid attribute number 5
 -- Change the definition of utrtest so that the foreign partition get updated
 -- after the local partition
 delete from utrtest;
@@ -8005,7 +8000,7 @@ update utrtest set a = 3 returning *;
    Update on public.locp utrtest_1
    Foreign Update on public.remp utrtest_2
    ->  Seq Scan on public.locp utrtest_1
-         Output: 3, utrtest_1.b, utrtest_1.ctid
+         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)
@@ -8023,19 +8018,19 @@ update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *;
    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.b, utrtest_1.ctid, "*VALUES*".*, "*VALUES*".column1
+         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.b, utrtest_1.ctid, utrtest_1.a
+               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.b, utrtest_2.ctid, "*VALUES*".*, "*VALUES*".column1
+         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.b, utrtest_2.ctid, utrtest_2.a
+               Output: utrtest_2.ctid, utrtest_2.*, utrtest_2.a
                Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE
          ->  Hash
                Output: "*VALUES*".*, "*VALUES*".column1
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 35b48575c5..6ba6786c8b 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -2322,32 +2322,26 @@ postgresPlanDirectModify(PlannerInfo *root,
 	 */
 	if (operation == CMD_UPDATE)
 	{
-		int			col;
+		ListCell *lc, *lc2;
 
 		/*
-		 * We transmit only columns that were explicitly targets of the
-		 * UPDATE, so as to avoid unnecessary data transmission.
+		 * The expressions of concern are the first N columns of the subplan
+		 * targetlist, where N is the length of root->update_colnos.
 		 */
-		col = -1;
-		while ((col = bms_next_member(rte->updatedCols, col)) >= 0)
+		targetAttrs = root->update_colnos;
+		forboth(lc, subplan->targetlist, lc2, targetAttrs)
 		{
-			/* bit numbers are offset by FirstLowInvalidHeapAttributeNumber */
-			AttrNumber	attno = col + FirstLowInvalidHeapAttributeNumber;
-			TargetEntry *tle;
+			TargetEntry *tle = lfirst_node(TargetEntry, lc);
+			AttrNumber attno = lfirst_int(lc2);
+
+			/* update's new-value expressions shouldn't be resjunk */
+			Assert(!tle->resjunk);
 
 			if (attno <= InvalidAttrNumber) /* shouldn't happen */
 				elog(ERROR, "system-column update is not supported");
 
-			tle = get_tle_by_resno(subplan->targetlist, attno);
-
-			if (!tle)
-				elog(ERROR, "attribute number %d not found in subplan targetlist",
-					 attno);
-
 			if (!is_foreign_expr(root, foreignrel, (Expr *) tle->expr))
 				return false;
-
-			targetAttrs = lappend_int(targetAttrs, attno);
 		}
 	}
 
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 04bc052ee8..6989957d50 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -703,10 +703,14 @@ ExecForeignUpdate(EState *estate,
      <literal>slot</literal> contains the new data for the tuple; it will match the
      row-type definition of the foreign table.
      <literal>planSlot</literal> contains the tuple that was generated by the
-     <structname>ModifyTable</structname> plan node's subplan; it differs from
-     <literal>slot</literal> in possibly containing additional <quote>junk</quote>
-     columns.  In particular, any junk columns that were requested by
-     <function>AddForeignUpdateTargets</function> will be available from this slot.
+     <structname>ModifyTable</structname> plan node's subplan.  Unlike
+     <literal>slot</literal>, this tuple contains only the new values for
+     columns changed by the query, so do not rely on attribute numbers of the
+     foreign table to index into <literal>planSlot</literal>.
+     Also, <literal>planSlot</literal> typically contains
+     additional <quote>junk</quote> columns.  In particular, any junk columns
+     that were requested by <function>AddForeignUpdateTargets</function> will
+     be available from this slot.
     </para>
 
     <para>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7383d5994e..a53070f602 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -2724,20 +2724,22 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
 		/*
 		 * In READ COMMITTED isolation level it's possible that target tuple
 		 * was changed due to concurrent update.  In that case we have a raw
-		 * subplan output tuple in epqslot_candidate, and need to run it
-		 * through the junk filter to produce an insertable tuple.
+		 * subplan output tuple in epqslot_candidate, and need to form a new
+		 * insertable tuple using ExecGetUpdateNewTuple to replace the one
+		 * we received in newslot.  Neither we nor our callers have any
+		 * further interest in the passed-in tuple, so it's okay to overwrite
+		 * newslot with the newer data.
 		 *
-		 * Caution: more than likely, the passed-in slot is the same as the
-		 * junkfilter's output slot, so we are clobbering the original value
-		 * of slottuple by doing the filtering.  This is OK since neither we
-		 * nor our caller have any more interest in the prior contents of that
-		 * slot.
+		 * (Typically, newslot was also generated by ExecGetUpdateNewTuple, so
+		 * that epqslot_clean will be that same slot and the copy step below
+		 * is not needed.)
 		 */
 		if (epqslot_candidate != NULL)
 		{
 			TupleTableSlot *epqslot_clean;
 
-			epqslot_clean = ExecFilterJunk(relinfo->ri_junkFilter, epqslot_candidate);
+			epqslot_clean = ExecGetUpdateNewTuple(relinfo, epqslot_candidate,
+												  oldslot);
 
 			if (newslot != epqslot_clean)
 				ExecCopySlot(newslot, epqslot_clean);
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 2e463f5499..a3937f3e66 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -477,6 +477,204 @@ ExecBuildProjectionInfo(List *targetList,
 	return projInfo;
 }
 
+/*
+ *		ExecBuildUpdateProjection
+ *
+ * Build a ProjectionInfo node for constructing a new tuple during UPDATE.
+ * The projection will be executed in the given econtext and the result will
+ * be stored into the given tuple slot.  (Caller must have ensured that tuple
+ * slot has a descriptor matching the target rel!)
+ *
+ * subTargetList is the tlist of the subplan node feeding ModifyTable.
+ * We use this mainly to cross-check that the expressions being assigned
+ * are of the correct types.  The values from this tlist are assumed to be
+ * available from the "outer" tuple slot.  They are assigned to target columns
+ * listed in the corresponding targetColnos elements.  (Only non-resjunk tlist
+ * entries are assigned.)  Columns not listed in targetColnos are filled from
+ * the UPDATE's old tuple, which is assumed to be available in the "scan"
+ * tuple slot.
+ *
+ * relDesc must describe the relation we intend to update.
+ *
+ * This is basically a specialized variant of ExecBuildProjectionInfo.
+ * However, it also performs sanity checks equivalent to ExecCheckPlanOutput.
+ * Since we never make a normal tlist equivalent to the whole
+ * tuple-to-be-assigned, there is no convenient way to apply
+ * ExecCheckPlanOutput, so we must do our safety checks here.
+ */
+ProjectionInfo *
+ExecBuildUpdateProjection(List *subTargetList,
+						  List *targetColnos,
+						  TupleDesc relDesc,
+						  ExprContext *econtext,
+						  TupleTableSlot *slot,
+						  PlanState *parent)
+{
+	ProjectionInfo *projInfo = makeNode(ProjectionInfo);
+	ExprState  *state;
+	int nAssignableCols;
+	bool sawJunk;
+	Bitmapset*assignedCols;
+	LastAttnumInfo deform = {0, 0, 0};
+	ExprEvalStep scratch = {0};
+	int outerattnum;
+	ListCell   *lc, *lc2;
+
+	projInfo->pi_exprContext = econtext;
+	/* We embed ExprState into ProjectionInfo instead of doing extra palloc */
+	projInfo->pi_state.tag = T_ExprState;
+	state = &projInfo->pi_state;
+	state->expr = NULL;			/* not used */
+	state->parent = parent;
+	state->ext_params = NULL;
+
+	state->resultslot = slot;
+
+	/*
+	 * Examine the subplan tlist to see how many non-junk columns there are,
+	 * and to verify that the non-junk columns come before the junk ones.
+	 */
+	nAssignableCols = 0;
+	sawJunk = false;
+	foreach(lc, subTargetList)
+	{
+		TargetEntry *tle = lfirst_node(TargetEntry, lc);
+
+		if (tle->resjunk)
+			sawJunk = true;
+		else
+		{
+			if (sawJunk)
+				elog(ERROR, "subplan target list is out of order");
+			nAssignableCols++;
+		}
+	}
+
+	/* We should have one targetColnos entry per non-junk column */
+	if (nAssignableCols != list_length(targetColnos))
+		elog(ERROR, "targetColnos does not match subplan target list");
+
+	/*
+	 * Build a bitmapset of the columns in targetColnos.  (We could just
+	 * use list_member_int() tests, but that risks O(N^2) behavior with
+	 * many columns.)
+	 */
+	assignedCols = NULL;
+	foreach(lc, targetColnos)
+	{
+		AttrNumber	targetattnum = lfirst_int(lc);
+
+		assignedCols = bms_add_member(assignedCols, targetattnum);
+	}
+
+	/*
+	 * We want to insert EEOP_*_FETCHSOME steps to ensure the outer and scan
+	 * tuples are sufficiently deconstructed.  Outer tuple is easy, but for
+	 * scan tuple we must find out the last old column we need.
+	 */
+	deform.last_outer = nAssignableCols;
+
+	for (int attnum = relDesc->natts; attnum > 0; attnum--)
+	{
+		Form_pg_attribute attr = TupleDescAttr(relDesc, attnum - 1);
+		if (attr->attisdropped)
+			continue;
+		if (bms_is_member(attnum, assignedCols))
+			continue;
+		deform.last_scan = attnum;
+		break;
+	}
+
+	ExecPushExprSlots(state, &deform);
+
+	/*
+	 * Now generate code to fetch data from the outer tuple, incidentally
+	 * validating that it'll be of the right type.  The checks above ensure
+	 * that the forboth() will iterate over exactly the non-junk columns.
+	 */
+	outerattnum = 0;
+	forboth(lc, subTargetList, lc2, targetColnos)
+	{
+		TargetEntry *tle = lfirst_node(TargetEntry, lc);
+		AttrNumber	targetattnum = lfirst_int(lc2);
+		Form_pg_attribute attr;
+
+		Assert(!tle->resjunk);
+
+		/*
+		 * Apply sanity checks comparable to ExecCheckPlanOutput().
+		 */
+		if (targetattnum <= 0 || targetattnum > relDesc->natts)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("table row type and query-specified row type do not match"),
+					 errdetail("Query has too many columns.")));
+		attr = TupleDescAttr(relDesc, targetattnum - 1);
+
+		if (attr->attisdropped)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("table row type and query-specified row type do not match"),
+					 errdetail("Query provides a value for a dropped column at ordinal position %d.",
+							   targetattnum)));
+		if (exprType((Node *) tle->expr) != attr->atttypid)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("table row type and query-specified row type do not match"),
+					 errdetail("Table has type %s at ordinal position %d, but query expects %s.",
+							   format_type_be(attr->atttypid),
+							   targetattnum,
+							   format_type_be(exprType((Node *) tle->expr)))));
+
+		/*
+		 * OK, build an outer-tuple reference.
+		 */
+		scratch.opcode = EEOP_ASSIGN_OUTER_VAR;
+		scratch.d.assign_var.attnum = outerattnum++;
+		scratch.d.assign_var.resultnum = targetattnum - 1;
+		ExprEvalPushStep(state, &scratch);
+	}
+
+	/*
+	 * Now generate code to copy over any old columns that were not assigned
+	 * to, and to ensure that dropped columns are set to NULL.
+	 */
+	for (int attnum = 1; attnum <= relDesc->natts; attnum++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(relDesc, attnum - 1);
+
+		if (attr->attisdropped)
+		{
+			/* Put a null into the ExprState's resvalue/resnull ... */
+			scratch.opcode = EEOP_CONST;
+			scratch.resvalue = &state->resvalue;
+			scratch.resnull = &state->resnull;
+			scratch.d.constval.value = (Datum) 0;
+			scratch.d.constval.isnull = true;
+			ExprEvalPushStep(state, &scratch);
+			/* ... then assign it to the result slot */
+			scratch.opcode = EEOP_ASSIGN_TMP;
+			scratch.d.assign_tmp.resultnum = attnum - 1;
+			ExprEvalPushStep(state, &scratch);
+		}
+		else if (!bms_is_member(attnum, assignedCols))
+		{
+			/* Certainly the right type, so needn't check */
+			scratch.opcode = EEOP_ASSIGN_SCAN_VAR;
+			scratch.d.assign_var.attnum = attnum - 1;
+			scratch.d.assign_var.resultnum = attnum - 1;
+			ExprEvalPushStep(state, &scratch);
+		}
+	}
+
+	scratch.opcode = EEOP_DONE;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return projInfo;
+}
+
 /*
  * ExecPrepareExpr --- initialize for expression execution outside a normal
  * Plan tree context.
diff --git a/src/backend/executor/execJunk.c b/src/backend/executor/execJunk.c
index 970e1c325e..2e0bcbbede 100644
--- a/src/backend/executor/execJunk.c
+++ b/src/backend/executor/execJunk.c
@@ -59,43 +59,16 @@
 JunkFilter *
 ExecInitJunkFilter(List *targetList, TupleTableSlot *slot)
 {
+	JunkFilter *junkfilter;
 	TupleDesc	cleanTupType;
+	int			cleanLength;
+	AttrNumber *cleanMap;
 
 	/*
 	 * Compute the tuple descriptor for the cleaned tuple.
 	 */
 	cleanTupType = ExecCleanTypeFromTL(targetList);
 
-	/*
-	 * The rest is the same as ExecInitJunkFilterInsertion, ie, we want to map
-	 * every non-junk targetlist column into the output tuple.
-	 */
-	return ExecInitJunkFilterInsertion(targetList, cleanTupType, slot);
-}
-
-/*
- * ExecInitJunkFilterInsertion
- *
- * Initialize a JunkFilter for insertions into a table.
- *
- * Here, we are given the target "clean" tuple descriptor rather than
- * inferring it from the targetlist.  Although the target descriptor can
- * contain deleted columns, that is not of concern here, since the targetlist
- * should contain corresponding NULL constants (cf. ExecCheckPlanOutput).
- * It is assumed that the caller has checked that the table's columns match up
- * with the non-junk columns of the targetlist.
- */
-JunkFilter *
-ExecInitJunkFilterInsertion(List *targetList,
-							TupleDesc cleanTupType,
-							TupleTableSlot *slot)
-{
-	JunkFilter *junkfilter;
-	int			cleanLength;
-	AttrNumber *cleanMap;
-	ListCell   *t;
-	AttrNumber	cleanResno;
-
 	/*
 	 * Use the given slot, or make a new slot if we weren't given one.
 	 */
@@ -117,6 +90,9 @@ ExecInitJunkFilterInsertion(List *targetList,
 	cleanLength = cleanTupType->natts;
 	if (cleanLength > 0)
 	{
+		AttrNumber	cleanResno;
+		ListCell   *t;
+
 		cleanMap = (AttrNumber *) palloc(cleanLength * sizeof(AttrNumber));
 		cleanResno = 0;
 		foreach(t, targetList)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 8de78ada63..ea1530e032 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1217,11 +1217,14 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 		resultRelInfo->ri_FdwRoutine = NULL;
 
 	/* The following fields are set later if needed */
+	resultRelInfo->ri_RowIdAttNo = 0;
+	resultRelInfo->ri_projectNew = NULL;
+	resultRelInfo->ri_newTupleSlot = NULL;
+	resultRelInfo->ri_oldTupleSlot = NULL;
 	resultRelInfo->ri_FdwState = NULL;
 	resultRelInfo->ri_usesFdwDirectModify = false;
 	resultRelInfo->ri_ConstraintExprs = NULL;
 	resultRelInfo->ri_GeneratedExprs = NULL;
-	resultRelInfo->ri_junkFilter = NULL;
 	resultRelInfo->ri_projectReturning = NULL;
 	resultRelInfo->ri_onConflictArbiterIndexes = NIL;
 	resultRelInfo->ri_onConflict = NULL;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 2993ba43e3..b9064bfe66 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -81,7 +81,7 @@ static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
 											   ResultRelInfo **partRelInfo);
 
 /*
- * Verify that the tuples to be produced by INSERT or UPDATE match the
+ * Verify that the tuples to be produced by INSERT match the
  * target relation's rowtype
  *
  * We do this to guard against stale plans.  If plan invalidation is
@@ -91,6 +91,9 @@ static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
  *
  * The plan output is represented by its targetlist, because that makes
  * handling the dropped-column case easier.
+ *
+ * We used to use this for UPDATE as well, but now the equivalent checks
+ * are done in ExecBuildUpdateProjection.
  */
 static void
 ExecCheckPlanOutput(Relation resultRel, List *targetList)
@@ -104,8 +107,7 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
 		TargetEntry *tle = (TargetEntry *) lfirst(lc);
 		Form_pg_attribute attr;
 
-		if (tle->resjunk)
-			continue;			/* ignore junk tlist items */
+		Assert(!tle->resjunk);	/* caller removed junk items already */
 
 		if (attno >= resultDesc->natts)
 			ereport(ERROR,
@@ -367,6 +369,55 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
 	MemoryContextSwitchTo(oldContext);
 }
 
+/*
+ * 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.
+ */
+static TupleTableSlot *
+ExecGetInsertNewTuple(ResultRelInfo *relinfo,
+					  TupleTableSlot *planSlot)
+{
+	ProjectionInfo *newProj = relinfo->ri_projectNew;
+	ExprContext   *econtext;
+
+	if (newProj == NULL)
+		return planSlot;
+
+	econtext = newProj->pi_exprContext;
+	econtext->ecxt_outertuple = planSlot;
+	return ExecProject(newProj);
+}
+
+/*
+ * 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.
+ */
+TupleTableSlot *
+ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
+					  TupleTableSlot *planSlot,
+					  TupleTableSlot *oldSlot)
+{
+	ProjectionInfo *newProj = relinfo->ri_projectNew;
+	ExprContext   *econtext;
+
+	Assert(newProj != NULL);
+	Assert(planSlot != NULL && !TTS_EMPTY(planSlot));
+	Assert(oldSlot != NULL && !TTS_EMPTY(oldSlot));
+
+	econtext = newProj->pi_exprContext;
+	econtext->ecxt_outertuple = planSlot;
+	econtext->ecxt_scantuple = oldSlot;
+	return ExecProject(newProj);
+}
+
+
 /* ----------------------------------------------------------------
  *		ExecInsert
  *
@@ -374,6 +425,10 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
  *		(or partition thereof) and insert appropriate tuples into the index
  *		relations.
  *
+ *		slot contains the new tuple value to be stored.
+ *		planSlot is the output of the ModifyTable's subplan; we use it
+ *		to access "junk" columns that are not going to be stored.
+ *
  *		Returns RETURNING result if any, otherwise NULL.
  *
  *		This may change the currently active tuple conversion map in
@@ -1194,7 +1249,9 @@ static bool
 ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 						 ResultRelInfo *resultRelInfo,
 						 ItemPointer tupleid, HeapTuple oldtuple,
-						 TupleTableSlot *slot, TupleTableSlot *planSlot,
+						 TupleTableSlot *slot,
+						 TupleTableSlot *oldSlot,
+						 TupleTableSlot *planSlot,
 						 EPQState *epqstate, bool canSetTag,
 						 TupleTableSlot **retry_slot,
 						 TupleTableSlot **inserted_tuple)
@@ -1269,7 +1326,15 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 			return true;
 		else
 		{
-			*retry_slot = ExecFilterJunk(resultRelInfo->ri_junkFilter, epqslot);
+			/* Fetch the most recent version of old tuple. */
+			ExecClearTuple(oldSlot);
+			if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
+											   tupleid,
+											   SnapshotAny,
+											   oldSlot))
+				elog(ERROR, "failed to fetch tuple being updated");
+			*retry_slot = ExecGetUpdateNewTuple(resultRelInfo, epqslot,
+												oldSlot);
 			return false;
 		}
 	}
@@ -1319,6 +1384,11 @@ 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.
+ *
  *		Returns RETURNING result if any, otherwise NULL.
  * ----------------------------------------------------------------
  */
@@ -1328,6 +1398,7 @@ ExecUpdate(ModifyTableState *mtstate,
 		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
+		   TupleTableSlot *oldSlot,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
 		   EState *estate,
@@ -1465,8 +1536,8 @@ lreplace:;
 			 * the tuple we're trying to move has been concurrently updated.
 			 */
 			retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
-											  oldtuple, slot, planSlot,
-											  epqstate, canSetTag,
+											  oldtuple, slot, oldSlot,
+											  planSlot, epqstate, canSetTag,
 											  &retry_slot, &inserted_tuple);
 			if (retry)
 			{
@@ -1578,7 +1649,15 @@ lreplace:;
 								/* Tuple not passing quals anymore, exiting... */
 								return NULL;
 
-							slot = ExecFilterJunk(resultRelInfo->ri_junkFilter, epqslot);
+							/* Fetch the most recent version of old tuple. */
+							ExecClearTuple(oldSlot);
+							if (!table_tuple_fetch_row_version(resultRelationDesc,
+															   tupleid,
+															   SnapshotAny,
+															   oldSlot))
+								elog(ERROR, "failed to fetch tuple being updated");
+							slot = ExecGetUpdateNewTuple(resultRelInfo,
+														 epqslot, oldSlot);
 							goto lreplace;
 
 						case TM_Deleted:
@@ -1874,7 +1953,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
 	/* Execute UPDATE with projection */
 	*returning = ExecUpdate(mtstate, resultRelInfo, conflictTid, NULL,
 							resultRelInfo->ri_onConflict->oc_ProjSlot,
-							planSlot,
+							existing, planSlot,
 							&mtstate->mt_epqstate, mtstate->ps.state,
 							canSetTag);
 
@@ -2051,7 +2130,6 @@ ExecModifyTable(PlanState *pstate)
 	CmdType		operation = node->operation;
 	ResultRelInfo *resultRelInfo;
 	PlanState  *subplanstate;
-	JunkFilter *junkfilter;
 	TupleTableSlot *slot;
 	TupleTableSlot *planSlot;
 	ItemPointer tupleid;
@@ -2097,7 +2175,6 @@ ExecModifyTable(PlanState *pstate)
 	/* Preload local variables */
 	resultRelInfo = node->resultRelInfo + node->mt_whichplan;
 	subplanstate = node->mt_plans[node->mt_whichplan];
-	junkfilter = resultRelInfo->ri_junkFilter;
 
 	/*
 	 * Fetch rows from subplan(s), and execute the required table modification
@@ -2131,7 +2208,6 @@ ExecModifyTable(PlanState *pstate)
 			{
 				resultRelInfo++;
 				subplanstate = node->mt_plans[node->mt_whichplan];
-				junkfilter = resultRelInfo->ri_junkFilter;
 				EvalPlanQualSetPlan(&node->mt_epqstate, subplanstate->plan,
 									node->mt_arowmarks[node->mt_whichplan]);
 				continue;
@@ -2173,87 +2249,123 @@ ExecModifyTable(PlanState *pstate)
 
 		tupleid = NULL;
 		oldtuple = NULL;
-		if (junkfilter != NULL)
+
+		/*
+		 * For UPDATE/DELETE, fetch the row identity info for the tuple to be
+		 * updated/deleted.  For a heap relation, that's a TID; otherwise we
+		 * may have a wholerow junk attr that carries the old tuple in toto.
+		 * Keep this in step with the part of ExecInitModifyTable that sets
+		 * up ri_RowIdAttNo.
+		 */
+		if (operation == CMD_UPDATE || operation == CMD_DELETE)
 		{
-			/*
-			 * extract the 'ctid' or 'wholerow' junk attribute.
-			 */
-			if (operation == CMD_UPDATE || operation == CMD_DELETE)
+			char		relkind;
+			Datum		datum;
+			bool		isNull;
+
+			relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
+			if (relkind == RELKIND_RELATION ||
+				relkind == RELKIND_MATVIEW ||
+				relkind == RELKIND_PARTITIONED_TABLE)
 			{
-				char		relkind;
-				Datum		datum;
-				bool		isNull;
-
-				relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
-				if (relkind == RELKIND_RELATION || relkind == RELKIND_MATVIEW)
-				{
-					datum = ExecGetJunkAttribute(slot,
-												 junkfilter->jf_junkAttNo,
-												 &isNull);
-					/* shouldn't ever get a null result... */
-					if (isNull)
-						elog(ERROR, "ctid is NULL");
-
-					tupleid = (ItemPointer) DatumGetPointer(datum);
-					tuple_ctid = *tupleid;	/* be sure we don't free ctid!! */
-					tupleid = &tuple_ctid;
-				}
-
-				/*
-				 * Use the wholerow attribute, when available, to reconstruct
-				 * the old relation tuple.
-				 *
-				 * Foreign table updates have a wholerow attribute when the
-				 * relation has a row-level trigger.  Note that the wholerow
-				 * attribute does not carry system columns.  Foreign table
-				 * triggers miss seeing those, except that we know enough here
-				 * to set t_tableOid.  Quite separately from this, the FDW may
-				 * fetch its own junk attrs to identify the row.
-				 *
-				 * Other relevant relkinds, currently limited to views, always
-				 * have a wholerow attribute.
-				 */
-				else if (AttributeNumberIsValid(junkfilter->jf_junkAttNo))
-				{
-					datum = ExecGetJunkAttribute(slot,
-												 junkfilter->jf_junkAttNo,
-												 &isNull);
-					/* shouldn't ever get a null result... */
-					if (isNull)
-						elog(ERROR, "wholerow is NULL");
-
-					oldtupdata.t_data = DatumGetHeapTupleHeader(datum);
-					oldtupdata.t_len =
-						HeapTupleHeaderGetDatumLength(oldtupdata.t_data);
-					ItemPointerSetInvalid(&(oldtupdata.t_self));
-					/* Historically, view triggers see invalid t_tableOid. */
-					oldtupdata.t_tableOid =
-						(relkind == RELKIND_VIEW) ? InvalidOid :
-						RelationGetRelid(resultRelInfo->ri_RelationDesc);
-
-					oldtuple = &oldtupdata;
-				}
-				else
-					Assert(relkind == RELKIND_FOREIGN_TABLE);
+				/* ri_RowIdAttNo refers to a ctid attribute */
+				Assert(AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo));
+				datum = ExecGetJunkAttribute(slot,
+											 resultRelInfo->ri_RowIdAttNo,
+											 &isNull);
+				/* shouldn't ever get a null result... */
+				if (isNull)
+					elog(ERROR, "ctid is NULL");
+
+				tupleid = (ItemPointer) DatumGetPointer(datum);
+				tuple_ctid = *tupleid;	/* be sure we don't free ctid!! */
+				tupleid = &tuple_ctid;
 			}
 
 			/*
-			 * apply the junkfilter if needed.
+			 * Use the wholerow attribute, when available, to reconstruct the
+			 * old relation tuple.  The old tuple serves one or both of two
+			 * purposes: 1) it serves as the OLD tuple for row triggers, 2) it
+			 * provides values for any unchanged columns for the NEW tuple of
+			 * an UPDATE, because the subplan does not produce all the columns
+			 * of the target table.
+			 *
+			 * Note that the wholerow attribute does not carry system columns,
+			 * so foreign table triggers miss seeing those, except that we
+			 * know enough here to set t_tableOid.  Quite separately from
+			 * this, the FDW may fetch its own junk attrs to identify the row.
+			 *
+			 * Other relevant relkinds, currently limited to views, always
+			 * have a wholerow attribute.
 			 */
-			if (operation != CMD_DELETE)
-				slot = ExecFilterJunk(junkfilter, slot);
+			else if (AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
+			{
+				datum = ExecGetJunkAttribute(slot,
+											 resultRelInfo->ri_RowIdAttNo,
+											 &isNull);
+				/* shouldn't ever get a null result... */
+				if (isNull)
+					elog(ERROR, "wholerow is NULL");
+
+				oldtupdata.t_data = DatumGetHeapTupleHeader(datum);
+				oldtupdata.t_len =
+					HeapTupleHeaderGetDatumLength(oldtupdata.t_data);
+				ItemPointerSetInvalid(&(oldtupdata.t_self));
+				/* Historically, view triggers see invalid t_tableOid. */
+				oldtupdata.t_tableOid =
+					(relkind == RELKIND_VIEW) ? InvalidOid :
+					RelationGetRelid(resultRelInfo->ri_RelationDesc);
+
+				oldtuple = &oldtupdata;
+			}
+			else
+			{
+				/* Only foreign tables are allowed to omit a row-ID attr */
+				Assert(relkind == RELKIND_FOREIGN_TABLE);
+			}
 		}
 
 		switch (operation)
 		{
 			case CMD_INSERT:
+				slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
 				slot = ExecInsert(node, resultRelInfo, slot, planSlot,
 								  estate, node->canSetTag);
 				break;
 			case CMD_UPDATE:
-				slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
-								  planSlot, &node->mt_epqstate, estate,
-								  node->canSetTag);
+				{
+					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);
+				}
 				break;
 			case CMD_DELETE:
 				slot = ExecDelete(node, resultRelInfo, tupleid, oldtuple,
@@ -2679,117 +2791,143 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 						mtstate->mt_arowmarks[0]);
 
 	/*
-	 * Initialize the junk filter(s) if needed.  INSERT queries need a filter
-	 * if there are any junk attrs in the tlist.  UPDATE and DELETE always
-	 * need a filter, since there's always at least one junk attribute present
-	 * --- no need to look first.  Typically, this will be a 'ctid' or
-	 * 'wholerow' attribute, but in the case of a foreign data wrapper it
-	 * might be a set of junk attributes sufficient to identify the remote
-	 * row.
+	 * Initialize projection(s) to create tuples suitable for result rel(s).
+	 * INSERT queries may need a projection to filter out junk attrs in the
+	 * tlist.  UPDATE always needs a projection, because (1) there's always
+	 * some junk attrs, and (2) we may need to merge values of not-updated
+	 * columns from the old tuple into the final tuple.  In UPDATE, the tuple
+	 * arriving from the subplan contains only new values for the changed
+	 * columns, plus row identity info in the junk attrs.
 	 *
-	 * If there are multiple result relations, each one needs its own junk
-	 * filter.  Note multiple rels are only possible for UPDATE/DELETE, so we
-	 * can't be fooled by some needing a filter and some not.
+	 * 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.
 	 *
 	 * 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++)
 	{
-		bool		junk_filter_needed = false;
+		resultRelInfo = &mtstate->resultRelInfo[i];
+		subplan = mtstate->mt_plans[i]->plan;
 
-		switch (operation)
+		/*
+		 * Prepare to generate tuples suitable for the target relation.
+		 */
+		if (operation == CMD_INSERT)
 		{
-			case CMD_INSERT:
-				foreach(l, subplan->targetlist)
-				{
-					TargetEntry *tle = (TargetEntry *) lfirst(l);
+			List	   *insertTargetList = NIL;
+			bool		need_projection = false;
+			foreach(l, subplan->targetlist)
+			{
+				TargetEntry *tle = (TargetEntry *) lfirst(l);
 
-					if (tle->resjunk)
-					{
-						junk_filter_needed = true;
-						break;
-					}
-				}
-				break;
-			case CMD_UPDATE:
-			case CMD_DELETE:
-				junk_filter_needed = true;
-				break;
-			default:
-				elog(ERROR, "unknown operation");
-				break;
-		}
+				if (!tle->resjunk)
+					insertTargetList = lappend(insertTargetList, tle);
+				else
+					need_projection = true;
+			}
+			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);
+
+				resultRelInfo->ri_projectNew =
+					ExecBuildProjectionInfo(insertTargetList,
+											mtstate->ps.ps_ExprContext,
+											resultRelInfo->ri_newTupleSlot,
+											&mtstate->ps,
+											relDesc);
+			}
 
-		if (junk_filter_needed)
+			/*
+			 * The junk-free list must produce a tuple suitable for the result
+			 * relation.
+			 */
+			ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
+								insertTargetList);
+		}
+		else if (operation == CMD_UPDATE)
 		{
-			resultRelInfo = mtstate->resultRelInfo;
-			for (i = 0; i < nplans; i++)
-			{
-				JunkFilter *j;
-				TupleTableSlot *junkresslot;
+			List	   *updateColnos;
+			TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
+
+			updateColnos = (List *) list_nth(node->updateColnosLists, i);
 
-				subplan = mtstate->mt_plans[i]->plan;
+			/*
+			 * For UPDATE, we use the old tuple to fill up missing values in
+			 * the tuple produced by the plan to get the new tuple.
+			 */
+			resultRelInfo->ri_oldTupleSlot =
+				table_slot_create(resultRelInfo->ri_RelationDesc,
+								  &mtstate->ps.state->es_tupleTable);
+			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);
+
+			resultRelInfo->ri_projectNew =
+				ExecBuildUpdateProjection(subplan->targetlist,
+										  updateColnos,
+										  relDesc,
+										  mtstate->ps.ps_ExprContext,
+										  resultRelInfo->ri_newTupleSlot,
+										  &mtstate->ps);
+		}
 
-				junkresslot =
-					ExecInitExtraTupleSlot(estate, NULL,
-										   table_slot_callbacks(resultRelInfo->ri_RelationDesc));
+		/*
+		 * For UPDATE/DELETE, find the appropriate junk attr now, either a
+		 * 'ctid' or 'wholerow' attribute depending on relkind.  For foreign
+		 * tables, the FDW might have created additional junk attr(s), but
+		 * those are no concern of ours.
+		 */
+		if (operation == CMD_UPDATE || operation == CMD_DELETE)
+		{
+			char	relkind;
 
+			relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
+			if (relkind == RELKIND_RELATION ||
+				relkind == RELKIND_MATVIEW ||
+				relkind == RELKIND_PARTITIONED_TABLE)
+			{
+				resultRelInfo->ri_RowIdAttNo =
+					ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid");
+				if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
+					elog(ERROR, "could not find junk ctid column");
+			}
+			else if (relkind == RELKIND_FOREIGN_TABLE)
+			{
 				/*
-				 * For an INSERT or UPDATE, the result tuple must always match
-				 * the target table's descriptor.  For a DELETE, it won't
-				 * (indeed, there's probably no non-junk output columns).
+				 * When there is a row-level trigger, there should be a
+				 * wholerow attribute.  We also require it to be present in
+				 * UPDATE, so we can get the values of unchanged columns.
 				 */
-				if (operation == CMD_INSERT || operation == CMD_UPDATE)
-				{
-					ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
-										subplan->targetlist);
-					j = ExecInitJunkFilterInsertion(subplan->targetlist,
-													RelationGetDescr(resultRelInfo->ri_RelationDesc),
-													junkresslot);
-				}
-				else
-					j = ExecInitJunkFilter(subplan->targetlist,
-										   junkresslot);
-
-				if (operation == CMD_UPDATE || operation == CMD_DELETE)
-				{
-					/* For UPDATE/DELETE, find the appropriate junk attr now */
-					char		relkind;
-
-					relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
-					if (relkind == RELKIND_RELATION ||
-						relkind == RELKIND_MATVIEW ||
-						relkind == RELKIND_PARTITIONED_TABLE)
-					{
-						j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
-						if (!AttributeNumberIsValid(j->jf_junkAttNo))
-							elog(ERROR, "could not find junk ctid column");
-					}
-					else if (relkind == RELKIND_FOREIGN_TABLE)
-					{
-						/*
-						 * When there is a row-level trigger, there should be
-						 * a wholerow attribute.
-						 */
-						j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
-					}
-					else
-					{
-						j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
-						if (!AttributeNumberIsValid(j->jf_junkAttNo))
-							elog(ERROR, "could not find junk wholerow column");
-					}
-				}
-
-				resultRelInfo->ri_junkFilter = j;
-				resultRelInfo++;
+				resultRelInfo->ri_RowIdAttNo =
+					ExecFindJunkAttributeInTlist(subplan->targetlist,
+												 "wholerow");
+				if (mtstate->operation == CMD_UPDATE &&
+					!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
+					elog(ERROR, "could not find junk wholerow column");
+			}
+			else
+			{
+				/* Other valid target relkinds must provide wholerow */
+				resultRelInfo->ri_RowIdAttNo =
+					ExecFindJunkAttributeInTlist(subplan->targetlist,
+												 "wholerow");
+				if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
+					elog(ERROR, "could not find junk wholerow column");
 			}
-		}
-		else
-		{
-			if (operation == CMD_INSERT)
-				ExecCheckPlanOutput(mtstate->resultRelInfo->ri_RelationDesc,
-									subplan->targetlist);
 		}
 	}
 
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 38b56231b7..1ec586729b 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -207,6 +207,7 @@ _copyModifyTable(const ModifyTable *from)
 	COPY_SCALAR_FIELD(partColsUpdated);
 	COPY_NODE_FIELD(resultRelations);
 	COPY_NODE_FIELD(plans);
+	COPY_NODE_FIELD(updateColnosLists);
 	COPY_NODE_FIELD(withCheckOptionLists);
 	COPY_NODE_FIELD(returningLists);
 	COPY_NODE_FIELD(fdwPrivLists);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 9f7918c7e9..99fb38c05a 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -408,6 +408,7 @@ _outModifyTable(StringInfo str, const ModifyTable *node)
 	WRITE_BOOL_FIELD(partColsUpdated);
 	WRITE_NODE_FIELD(resultRelations);
 	WRITE_NODE_FIELD(plans);
+	WRITE_NODE_FIELD(updateColnosLists);
 	WRITE_NODE_FIELD(withCheckOptionLists);
 	WRITE_NODE_FIELD(returningLists);
 	WRITE_NODE_FIELD(fdwPrivLists);
@@ -2143,6 +2144,7 @@ _outModifyTablePath(StringInfo str, const ModifyTablePath *node)
 	WRITE_NODE_FIELD(resultRelations);
 	WRITE_NODE_FIELD(subpaths);
 	WRITE_NODE_FIELD(subroots);
+	WRITE_NODE_FIELD(updateColnosLists);
 	WRITE_NODE_FIELD(withCheckOptionLists);
 	WRITE_NODE_FIELD(returningLists);
 	WRITE_NODE_FIELD(rowMarks);
@@ -2268,12 +2270,12 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_NODE_FIELD(distinct_pathkeys);
 	WRITE_NODE_FIELD(sort_pathkeys);
 	WRITE_NODE_FIELD(processed_tlist);
+	WRITE_NODE_FIELD(update_colnos);
 	WRITE_NODE_FIELD(minmax_aggs);
 	WRITE_FLOAT_FIELD(total_table_pages, "%.0f");
 	WRITE_FLOAT_FIELD(tuple_fraction, "%.4f");
 	WRITE_FLOAT_FIELD(limit_tuples, "%.0f");
 	WRITE_UINT_FIELD(qual_security_level);
-	WRITE_ENUM_FIELD(inhTargetKind, InheritanceKind);
 	WRITE_BOOL_FIELD(hasJoinRTEs);
 	WRITE_BOOL_FIELD(hasLateralRTEs);
 	WRITE_BOOL_FIELD(hasHavingQual);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 377185f7c6..0b6331d3da 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1683,6 +1683,7 @@ _readModifyTable(void)
 	READ_BOOL_FIELD(partColsUpdated);
 	READ_NODE_FIELD(resultRelations);
 	READ_NODE_FIELD(plans);
+	READ_NODE_FIELD(updateColnosLists);
 	READ_NODE_FIELD(withCheckOptionLists);
 	READ_NODE_FIELD(returningLists);
 	READ_NODE_FIELD(fdwPrivLists);
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 906cab7053..4bb482879f 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -302,6 +302,7 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 									 Index nominalRelation, Index rootRelation,
 									 bool partColsUpdated,
 									 List *resultRelations, List *subplans, List *subroots,
+									 List *updateColnosLists,
 									 List *withCheckOptionLists, List *returningLists,
 									 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
@@ -2642,7 +2643,8 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 	ModifyTable *plan;
 	List	   *subplans = NIL;
 	ListCell   *subpaths,
-			   *subroots;
+			   *subroots,
+			   *lc;
 
 	/* Build the plan for each input path */
 	forboth(subpaths, best_path->subpaths,
@@ -2665,9 +2667,6 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 		 */
 		subplan = create_plan_recurse(subroot, subpath, CP_EXACT_TLIST);
 
-		/* Transfer resname/resjunk labeling, too, to keep executor happy */
-		apply_tlist_labeling(subplan->targetlist, subroot->processed_tlist);
-
 		subplans = lappend(subplans, subplan);
 	}
 
@@ -2680,6 +2679,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 							best_path->resultRelations,
 							subplans,
 							best_path->subroots,
+							best_path->updateColnosLists,
 							best_path->withCheckOptionLists,
 							best_path->returningLists,
 							best_path->rowMarks,
@@ -2688,6 +2688,41 @@ 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;
 }
 
@@ -6880,6 +6915,7 @@ make_modifytable(PlannerInfo *root,
 				 Index nominalRelation, Index rootRelation,
 				 bool partColsUpdated,
 				 List *resultRelations, List *subplans, List *subroots,
+				 List *updateColnosLists,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam)
 {
@@ -6892,6 +6928,9 @@ make_modifytable(PlannerInfo *root,
 
 	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);
 	Assert(withCheckOptionLists == NIL ||
 		   list_length(resultRelations) == list_length(withCheckOptionLists));
 	Assert(returningLists == NIL ||
@@ -6936,6 +6975,7 @@ make_modifytable(PlannerInfo *root,
 		node->exclRelRTI = onconflict->exclRelIndex;
 		node->exclRelTlist = onconflict->exclRelTlist;
 	}
+	node->updateColnosLists = updateColnosLists;
 	node->withCheckOptionLists = withCheckOptionLists;
 	node->returningLists = returningLists;
 	node->rowMarks = rowMarks;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index f529d107d2..ccb9166a8e 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -620,6 +620,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	memset(root->upper_rels, 0, sizeof(root->upper_rels));
 	memset(root->upper_targets, 0, sizeof(root->upper_targets));
 	root->processed_tlist = NIL;
+	root->update_colnos = NIL;
 	root->grouping_map = NULL;
 	root->minmax_aggs = NIL;
 	root->qual_security_level = 0;
@@ -1222,6 +1223,7 @@ inheritance_planner(PlannerInfo *root)
 	List	   *subpaths = NIL;
 	List	   *subroots = NIL;
 	List	   *resultRelations = NIL;
+	List	   *updateColnosLists = NIL;
 	List	   *withCheckOptionLists = NIL;
 	List	   *returningLists = NIL;
 	List	   *rowMarks;
@@ -1687,6 +1689,11 @@ inheritance_planner(PlannerInfo *root)
 		/* 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,
@@ -1732,6 +1739,9 @@ inheritance_planner(PlannerInfo *root)
 		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)
@@ -1788,6 +1798,7 @@ inheritance_planner(PlannerInfo *root)
 									 resultRelations,
 									 subpaths,
 									 subroots,
+									 updateColnosLists,
 									 withCheckOptionLists,
 									 returningLists,
 									 rowMarks,
@@ -2313,6 +2324,7 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 		if (parse->commandType != CMD_SELECT && !inheritance_update)
 		{
 			Index		rootRelation;
+			List *updateColnosLists;
 			List	   *withCheckOptionLists;
 			List	   *returningLists;
 			List	   *rowMarks;
@@ -2327,6 +2339,12 @@ 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.
@@ -2361,6 +2379,7 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 										list_make1_int(parse->resultRelation),
 										list_make1(path),
 										list_make1(root),
+										updateColnosLists,
 										withCheckOptionLists,
 										returningLists,
 										rowMarks,
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index d961592e01..e18553ac7c 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -925,6 +925,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	memset(subroot->upper_rels, 0, sizeof(subroot->upper_rels));
 	memset(subroot->upper_targets, 0, sizeof(subroot->upper_targets));
 	subroot->processed_tlist = NIL;
+	subroot->update_colnos = NIL;
 	subroot->grouping_map = NULL;
 	subroot->minmax_aggs = NIL;
 	subroot->qual_security_level = 0;
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index 23f9f861f4..488e8cfd4d 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -3,13 +3,19 @@
  * preptlist.c
  *	  Routines to preprocess the parse tree target list
  *
- * For INSERT and UPDATE queries, the targetlist must contain an entry for
- * each attribute of the target relation in the correct order.  For UPDATE and
- * DELETE queries, it must also contain junk tlist entries needed to allow the
- * executor to identify the rows to be updated or deleted.  For all query
- * types, we may need to add junk tlist entries for Vars used in the RETURNING
- * list and row ID information needed for SELECT FOR UPDATE locking and/or
- * EvalPlanQual checking.
+ * For an INSERT, the targetlist must contain an entry for each attribute of
+ * the target relation in the correct order.
+ *
+ * For an UPDATE, the targetlist just contains the expressions for the new
+ * column values.
+ *
+ * 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.
+ *
+ * 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
+ * for SELECT FOR UPDATE locking and/or EvalPlanQual checking.
  *
  * The query rewrite phase also does preprocessing of the targetlist (see
  * rewriteTargetListIU).  The division of labor between here and there is
@@ -52,6 +58,7 @@
 #include "rewrite/rewriteHandler.h"
 #include "utils/rel.h"
 
+static List *make_update_colnos(List *tlist);
 static List *expand_targetlist(List *tlist, int command_type,
 							   Index result_relation, Relation rel);
 
@@ -63,7 +70,8 @@ static List *expand_targetlist(List *tlist, int command_type,
  *	  Returns the new targetlist.
  *
  * As a side effect, if there's an ON CONFLICT UPDATE clause, its targetlist
- * is also preprocessed (and updated in-place).
+ * 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.
  */
 List *
 preprocess_targetlist(PlannerInfo *root)
@@ -108,14 +116,19 @@ preprocess_targetlist(PlannerInfo *root)
 		rewriteTargetListUD(parse, target_rte, target_relation);
 
 	/*
-	 * for heap_form_tuple to work, the targetlist must match the exact order
-	 * of the attributes. We also need to fill in any missing attributes. -ay
-	 * 10/94
+	 * 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.
 	 */
 	tlist = parse->targetList;
-	if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+	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);
 
 	/*
 	 * Add necessary junk columns for rowmarked rels.  These values are needed
@@ -239,6 +252,29 @@ preprocess_targetlist(PlannerInfo *root)
 	return tlist;
 }
 
+/*
+ * make_update_colnos
+ * 		Extract a list of the target-table column numbers that
+ * 		an UPDATE's targetlist wants to assign to.
+ *
+ * We just need to capture the resno's of the non-junk tlist entries.
+ */
+static List *
+make_update_colnos(List *tlist)
+{
+	List*update_colnos = NIL;
+	ListCell *lc;
+
+	foreach(lc, tlist)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+		if (!tle->resjunk)
+			update_colnos = lappend_int(update_colnos, tle->resno);
+	}
+	return update_colnos;
+}
+
 
 /*****************************************************************************
  *
@@ -251,6 +287,10 @@ preprocess_targetlist(PlannerInfo *root)
  *	  Given a target list as generated by the parser and a result relation,
  *	  add targetlist entries for any missing attributes, and ensure the
  *	  non-junk attributes appear in proper field order.
+ *
+ * command_type is a bit of an archaism now: it's CMD_INSERT when we're
+ * processing an INSERT, all right, but the only other use of this function
+ * is for ON CONFLICT UPDATE tlists, for which command_type is CMD_UPDATE.
  */
 static List *
 expand_targetlist(List *tlist, int command_type,
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 69b83071cf..a97929c13f 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3548,6 +3548,8 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
  * '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)
  * 'returningLists' is a list of RETURNING tlists (one per rel)
  * 'rowMarks' is a list of PlanRowMarks (non-locking only)
@@ -3561,6 +3563,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 						bool partColsUpdated,
 						List *resultRelations, List *subpaths,
 						List *subroots,
+						List *updateColnosLists,
 						List *withCheckOptionLists, List *returningLists,
 						List *rowMarks, OnConflictExpr *onconflict,
 						int epqParam)
@@ -3571,6 +3574,9 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
 	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);
 	Assert(withCheckOptionLists == NIL ||
 		   list_length(resultRelations) == list_length(withCheckOptionLists));
 	Assert(returningLists == NIL ||
@@ -3633,6 +3639,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 	pathnode->resultRelations = resultRelations;
 	pathnode->subpaths = subpaths;
 	pathnode->subroots = subroots;
+	pathnode->updateColnosLists = updateColnosLists;
 	pathnode->withCheckOptionLists = withCheckOptionLists;
 	pathnode->returningLists = returningLists;
 	pathnode->rowMarks = rowMarks;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 0672f497c6..f9175987f8 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1659,17 +1659,21 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
 												target_relation);
 
 		/*
-		 * If we have a row-level trigger corresponding to the operation, emit
-		 * a whole-row Var so that executor will have the "old" row to pass to
-		 * the trigger.  Alas, this misses system columns.
+		 * 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 (target_relation->trigdesc &&
-			((parsetree->commandType == CMD_UPDATE &&
-			  (target_relation->trigdesc->trig_update_after_row ||
-			   target_relation->trigdesc->trig_update_before_row)) ||
-			 (parsetree->commandType == CMD_DELETE &&
-			  (target_relation->trigdesc->trig_delete_after_row ||
-			   target_relation->trigdesc->trig_delete_before_row))))
+		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,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 071e363d54..c8c09f1cb5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -156,9 +156,6 @@ extern void ResetTupleHashTable(TupleHashTable hashtable);
  */
 extern JunkFilter *ExecInitJunkFilter(List *targetList,
 									  TupleTableSlot *slot);
-extern JunkFilter *ExecInitJunkFilterInsertion(List *targetList,
-											   TupleDesc cleanTupType,
-											   TupleTableSlot *slot);
 extern JunkFilter *ExecInitJunkFilterConversion(List *targetList,
 												TupleDesc cleanTupType,
 												TupleTableSlot *slot);
@@ -270,6 +267,12 @@ extern ProjectionInfo *ExecBuildProjectionInfo(List *targetList,
 											   TupleTableSlot *slot,
 											   PlanState *parent,
 											   TupleDesc inputDesc);
+extern ProjectionInfo *ExecBuildUpdateProjection(List *subTargetList,
+						  List *targetColnos,
+						  TupleDesc relDesc,
+						  ExprContext *econtext,
+						  TupleTableSlot *slot,
+						  PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
@@ -622,4 +625,9 @@ extern void CheckCmdReplicaIdentity(Relation rel, CmdType cmd);
 extern void CheckSubscriptionRelkind(char relkind, const char *nspname,
 									 const char *relname);
 
+/* needed by trigger.c */
+extern TupleTableSlot *ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
+						  TupleTableSlot *planSlot,
+						  TupleTableSlot *oldSlot);
+
 #endif							/* EXECUTOR_H  */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e31ad6204e..7af6d48525 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -356,10 +356,6 @@ typedef struct ProjectionInfo
  *						attribute numbers of the "original" tuple and the
  *						attribute numbers of the "clean" tuple.
  *	  resultSlot:		tuple slot used to hold cleaned tuple.
- *	  junkAttNo:		not used by junkfilter code.  Can be used by caller
- *						to remember the attno of a specific junk attribute
- *						(nodeModifyTable.c keeps the "ctid" or "wholerow"
- *						attno here).
  * ----------------
  */
 typedef struct JunkFilter
@@ -369,7 +365,6 @@ typedef struct JunkFilter
 	TupleDesc	jf_cleanTupType;
 	AttrNumber *jf_cleanMap;
 	TupleTableSlot *jf_resultSlot;
-	AttrNumber	jf_junkAttNo;
 } JunkFilter;
 
 /*
@@ -423,6 +418,19 @@ typedef struct ResultRelInfo
 	/* array of key/attr info for indices */
 	IndexInfo **ri_IndexRelationInfo;
 
+	/*
+	 * For UPDATE/DELETE result relations, the attribute number of the row
+	 * identity junk attribute in the source plan's output tuples
+	 */
+	AttrNumber		ri_RowIdAttNo;
+
+	/* Projection to generate new tuple in an INSERT/UPDATE */
+	ProjectionInfo *ri_projectNew;
+	/* Slot to hold that tuple */
+	TupleTableSlot *ri_newTupleSlot;
+	/* Slot to hold the old tuple being updated */
+	TupleTableSlot *ri_oldTupleSlot;
+
 	/* triggers to be fired, if any */
 	TriggerDesc *ri_TrigDesc;
 
@@ -470,9 +478,6 @@ typedef struct ResultRelInfo
 	/* number of stored generated columns we need to compute */
 	int			ri_NumGeneratedNeeded;
 
-	/* for removing junk attributes from tuples */
-	JunkFilter *ri_junkFilter;
-
 	/* list of RETURNING expressions */
 	List	   *ri_returningList;
 
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index c13642e35e..bed9f4da09 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -309,15 +309,23 @@ struct PlannerInfo
 
 	/*
 	 * The fully-processed targetlist is kept here.  It differs from
-	 * parse->targetList in that (for INSERT and UPDATE) it's been reordered
-	 * to match the target table, and defaults have been filled in.  Also,
-	 * additional resjunk targets may be present.  preprocess_targetlist()
-	 * does most of this work, but note that more resjunk targets can get
-	 * added during appendrel expansion.  (Hence, upper_targets mustn't get
-	 * set up till after that.)
+	 * parse->targetList in that (for INSERT) it's been reordered to match the
+	 * target table, and defaults have been filled in.  Also, additional
+	 * resjunk targets may be present.  preprocess_targetlist() does most of
+	 * that work, but note that more resjunk targets can get added during
+	 * appendrel expansion.  (Hence, upper_targets mustn't get set up till
+	 * after that.)
 	 */
 	List	   *processed_tlist;
 
+	/*
+	 * 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.)
+	 */
+	List	   *update_colnos;
+
 	/* Fields filled during create_plan() for use in setrefs.c */
 	AttrNumber *grouping_map;	/* for GroupingFunc fixup */
 	List	   *minmax_aggs;	/* List of MinMaxAggInfos */
@@ -1839,6 +1847,7 @@ typedef struct ModifyTablePath
 	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 */
 	List	   *rowMarks;		/* PlanRowMarks (non-locking only) */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 6e62104d0b..7d74bd92b8 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -219,6 +219,7 @@ typedef struct ModifyTable
 	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 */
 	List	   *fdwPrivLists;	/* per-target-table FDW private data lists */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 54f4b782fc..9673a4a638 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -265,6 +265,7 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
 												bool partColsUpdated,
 												List *resultRelations, List *subpaths,
 												List *subroots,
+												List *updateColnosLists,
 												List *withCheckOptionLists, List *returningLists,
 												List *rowMarks, OnConflictExpr *onconflict,
 												int epqParam);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 2b68aef654..94e43c3410 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -545,25 +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), b, ctid
+         Output: (a + 1), ctid
          One-Time Filter: false
 (5 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
    ->  Result
-         Output: (a + 1), b, ctid
+         Output: (a + 1), ctid
          One-Time Filter: false
 (6 rows)
 
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 24905332b1..770eab38b5 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -1283,12 +1283,12 @@ SELECT * FROM rw_view1;
 (4 rows)
 
 EXPLAIN (verbose, costs off) UPDATE rw_view1 SET b = b + 1 RETURNING *;
-                         QUERY PLAN                          
--------------------------------------------------------------
+                   QUERY PLAN                    
+-------------------------------------------------
  Update on public.base_tbl
    Output: base_tbl.a, base_tbl.b
    ->  Seq Scan on public.base_tbl
-         Output: base_tbl.a, (base_tbl.b + 1), base_tbl.ctid
+         Output: (base_tbl.b + 1), base_tbl.ctid
 (4 rows)
 
 UPDATE rw_view1 SET b = b + 1 RETURNING *;
@@ -2340,7 +2340,7 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
    Update on public.t12 t1_2
    Update on public.t111 t1_3
    ->  Index Scan using t1_a_idx on public.t1
-         Output: 100, t1.b, t1.c, t1.ctid
+         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
@@ -2350,15 +2350,15 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
                  ->  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.b, t1_1.c, t1_1.d, t1_1.ctid
+         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.b, t1_2.c, t1_2.e, t1_2.ctid
+         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.b, t1_3.c, t1_3.d, t1_3.e, t1_3.ctid
+         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)
@@ -2376,15 +2376,15 @@ 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.b, t1.c, t1.ctid
+         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
@@ -2394,15 +2394,15 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                  ->  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.b, t1_1.c, t1_1.d, t1_1.ctid
+         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.b, t1_2.c, t1_2.e, t1_2.ctid
+         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.b, t1_3.c, t1_3.d, t1_3.e, t1_3.ctid
+         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)
diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out
index bf939d79f6..dece036069 100644
--- a/src/test/regress/expected/update.out
+++ b/src/test/regress/expected/update.out
@@ -172,14 +172,14 @@ EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE update_test t
   SET (a, b) = (SELECT b, a FROM update_test s WHERE s.a = t.a)
   WHERE CURRENT_USER = SESSION_USER;
-                            QUERY PLAN                            
-------------------------------------------------------------------
+                         QUERY PLAN                          
+-------------------------------------------------------------
  Update on public.update_test t
    ->  Result
-         Output: $1, $2, t.c, (SubPlan 1 (returns $1,$2)), t.ctid
+         Output: $1, $2, (SubPlan 1 (returns $1,$2)), t.ctid
          One-Time Filter: (CURRENT_USER = SESSION_USER)
          ->  Seq Scan on public.update_test t
-               Output: t.c, t.a, t.ctid
+               Output: t.a, t.ctid
          SubPlan 1 (returns $1,$2)
            ->  Seq Scan on public.update_test s
                  Output: s.b, s.a
