From ffb154c58a2144e494b2d7d26b237ab51be341af Mon Sep 17 00:00:00 2001 From: Rui Zhao Date: Wed, 7 Oct 2026 19:17:04 +0800 Subject: [PATCH 1/2] Fix missing remote projection costs with local HAVING Remote projection costs omit rows expected to be filtered out by local HAVING. Those rows are still evaluated on the remote server, so charge the projection using retrieved_rows rather than rows. Add tests that keep the remote query unchanged and verify that all fetched rows are charged when the function COST increases. --- .../postgres_fdw/expected/postgres_fdw.out | 53 +++++++++++++++++++ contrib/postgres_fdw/postgres_fdw.c | 2 +- contrib/postgres_fdw/sql/postgres_fdw.sql | 43 +++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 416ca1d9c1e..476f7ba7abb 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -318,6 +318,59 @@ ORDER BY c2; Remote SQL: SELECT c2, count(*) FROM "S 1"."T 1" GROUP BY 1 ORDER BY c2 ASC NULLS LAST (5 rows) +-- Local HAVING must not reduce the cost of remote projection. +CREATE FUNCTION remote_value(bigint) RETURNS bigint +LANGUAGE plpgsql IMMUTABLE COST 100 AS $$ +BEGIN + RETURN $1; +END +$$; +ALTER EXTENSION postgres_fdw ADD FUNCTION remote_value(bigint); +ALTER SERVER loopback OPTIONS (ADD extensions 'postgres_fdw'); +CREATE FUNCTION fdw_cost_plan(query text) RETURNS json +LANGUAGE plpgsql AS $$ +DECLARE + result json; +BEGIN + EXECUTE 'EXPLAIN (VERBOSE, FORMAT JSON) ' || query INTO result; + RETURN result->0->'Plan'; +END +$$; +SELECT (plan->>'Startup Cost')::numeric AS remote_startup, + (plan->>'Plan Rows')::numeric AS remote_rows, + plan->>'Remote SQL' AS remote_sql +FROM fdw_cost_plan('SELECT c2, remote_value(sum(c1)), count(*) FROM ft1 + GROUP BY c2 ORDER BY c2') AS plan +\gset +SELECT plan->>'Node Type' AS node_type, + (plan->>'Startup Cost')::numeric = :remote_startup AS same_startup, + plan->>'Remote SQL' = :'remote_sql' AS same_remote_sql +FROM fdw_cost_plan('SELECT c2, remote_value(sum(c1)), count(*) FROM ft1 + GROUP BY c2 HAVING local_filter(count(*)::int) + ORDER BY c2') AS plan; + node_type | same_startup | same_remote_sql +--------------+--------------+----------------- + Foreign Scan | t | t +(1 row) + +-- Raising the remote function cost must still charge every fetched group. +ALTER FUNCTION remote_value(bigint) COST 200; +SELECT plan->>'Node Type' AS node_type, + (plan->>'Startup Cost')::numeric - :remote_startup = + :remote_rows * 100 * current_setting('cpu_operator_cost')::numeric + AS all_groups_charged +FROM fdw_cost_plan('SELECT c2, remote_value(sum(c1)), count(*) FROM ft1 + GROUP BY c2 HAVING local_filter(count(*)::int) + ORDER BY c2') AS plan; + node_type | all_groups_charged +--------------+-------------------- + Foreign Scan | t +(1 row) + +DROP FUNCTION fdw_cost_plan(text); +ALTER EXTENSION postgres_fdw DROP FUNCTION remote_value(bigint); +DROP FUNCTION remote_value(bigint); +ALTER SERVER loopback OPTIONS (DROP extensions); DROP FUNCTION local_filter(int); DROP FUNCTION local_project(int); -- =================================================================== diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 6d6ab64ce92..f46d453332e 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -3722,7 +3722,7 @@ estimate_path_cost_size(PlannerInfo *root, /* Add in eval cost of the remotely computed tlist expressions */ startup_cost += remote_tlist_cost.startup; - run_cost += remote_tlist_cost.per_tuple * rows; + run_cost += remote_tlist_cost.per_tuple * retrieved_rows; } else { diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 7d3c79b86fe..6872a75ef4e 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -276,6 +276,49 @@ SELECT local_project(c2), count(*) FROM ft1 GROUP BY c2 ORDER BY c2; EXPLAIN (VERBOSE, COSTS OFF) SELECT c2, count(*) FROM ft1 GROUP BY c2 HAVING local_filter(count(*)::int) ORDER BY c2; +-- Local HAVING must not reduce the cost of remote projection. +CREATE FUNCTION remote_value(bigint) RETURNS bigint +LANGUAGE plpgsql IMMUTABLE COST 100 AS $$ +BEGIN + RETURN $1; +END +$$; +ALTER EXTENSION postgres_fdw ADD FUNCTION remote_value(bigint); +ALTER SERVER loopback OPTIONS (ADD extensions 'postgres_fdw'); +CREATE FUNCTION fdw_cost_plan(query text) RETURNS json +LANGUAGE plpgsql AS $$ +DECLARE + result json; +BEGIN + EXECUTE 'EXPLAIN (VERBOSE, FORMAT JSON) ' || query INTO result; + RETURN result->0->'Plan'; +END +$$; +SELECT (plan->>'Startup Cost')::numeric AS remote_startup, + (plan->>'Plan Rows')::numeric AS remote_rows, + plan->>'Remote SQL' AS remote_sql +FROM fdw_cost_plan('SELECT c2, remote_value(sum(c1)), count(*) FROM ft1 + GROUP BY c2 ORDER BY c2') AS plan +\gset +SELECT plan->>'Node Type' AS node_type, + (plan->>'Startup Cost')::numeric = :remote_startup AS same_startup, + plan->>'Remote SQL' = :'remote_sql' AS same_remote_sql +FROM fdw_cost_plan('SELECT c2, remote_value(sum(c1)), count(*) FROM ft1 + GROUP BY c2 HAVING local_filter(count(*)::int) + ORDER BY c2') AS plan; +-- Raising the remote function cost must still charge every fetched group. +ALTER FUNCTION remote_value(bigint) COST 200; +SELECT plan->>'Node Type' AS node_type, + (plan->>'Startup Cost')::numeric - :remote_startup = + :remote_rows * 100 * current_setting('cpu_operator_cost')::numeric + AS all_groups_charged +FROM fdw_cost_plan('SELECT c2, remote_value(sum(c1)), count(*) FROM ft1 + GROUP BY c2 HAVING local_filter(count(*)::int) + ORDER BY c2') AS plan; +DROP FUNCTION fdw_cost_plan(text); +ALTER EXTENSION postgres_fdw DROP FUNCTION remote_value(bigint); +DROP FUNCTION remote_value(bigint); +ALTER SERVER loopback OPTIONS (DROP extensions); DROP FUNCTION local_filter(int); DROP FUNCTION local_project(int);