From 0fd1609afe255b275c2560db6db584081a16e8a0 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Mon, 24 Aug 2026 18:50:02 +0300 Subject: [PATCH] postgres_fdw: Fix flaky push down FUNCTION RTE test The unnest() test introduced in 0ee83dd4a99 did not produce a deterministic EXPLAIN plan output. The two sides of this test's nested loop join cost about the same, so which one ended up as the outer wasn't stable and could flip between platforms. Use an equality predicate on ft1.c3 instead of a range one. That restricts ft1 to a single row, so it's clearly the outer of the nested loop and the Materialize on the inner side goes away, giving a stable plan. Author: Sami Imseih Reviewed-by: Bharath Rupireddy Discussion: https://postgr.es/m/CAA5RZ0uC%3DGOihOKcF7o3JVETF3e-Fkc4OUO5NbqMEKCTeSN-dQ%40mail.gmail.com --- .../postgres_fdw/expected/postgres_fdw.out | 24 +++++++++---------- contrib/postgres_fdw/sql/postgres_fdw.sql | 8 +++---- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 7c09c52b8ea..517d15cf1fa 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -3128,28 +3128,26 @@ WHERE t1.c1 = u.id AND t2.c1 = u.id; Remote SQL: SELECT c1 FROM "S 1"."T 4" (12 rows) --- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a --- handful of remote rows; now ft6 is effectively the bigger side and the --- function is absorbed into ft6 instead. +-- Equality on ft1.c3 (not in the eqclass) restricts ft1 to a single remote row; +-- now ft6 is effectively the bigger side and the function is absorbed into +-- ft6 instead. EXPLAIN (VERBOSE, COSTS OFF) SELECT t1.c1, t2.c1 FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id) -WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010'; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------- +WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 = '00009'; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------- Nested Loop Output: t1.c1, t2.c1 Join Filter: (t1.c1 = u.id) -> Foreign Scan on public.ft1 t1 Output: t1.c1 - Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 < '00010')) - -> Materialize + Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 = '00009')) + -> Foreign Scan Output: t2.c1, u.id - -> Foreign Scan - Output: t2.c1, u.id - Relations: (public.ft6 t2) INNER JOIN (pg_catalog.unnest() u) - Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1)))) -(12 rows) + Relations: (public.ft6 t2) INNER JOIN (pg_catalog.unnest() u) + Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1)))) +(10 rows) -- The remaining scenarios reuse a dedicated foreign table to cover the -- corner cases of FUNCTION RTE push-down: function-first FROM, record diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 6571a18ba0c..ec766e2b28a 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -883,13 +883,13 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT t1.c1, t2.c1 FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id) WHERE t1.c1 = u.id AND t2.c1 = u.id; --- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a --- handful of remote rows; now ft6 is effectively the bigger side and the --- function is absorbed into ft6 instead. +-- Equality on ft1.c3 (not in the eqclass) restricts ft1 to a single remote row; +-- now ft6 is effectively the bigger side and the function is absorbed into +-- ft6 instead. EXPLAIN (VERBOSE, COSTS OFF) SELECT t1.c1, t2.c1 FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id) -WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010'; +WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 = '00009'; -- The remaining scenarios reuse a dedicated foreign table to cover the -- corner cases of FUNCTION RTE push-down: function-first FROM, record