From 8234e236f1ef82ebb41162eddf71137a2dd21a82 Mon Sep 17 00:00:00 2001 From: Rui Zhao Date: Fri, 2 Oct 2026 23:21:32 +0800 Subject: [PATCH 1/2] Keep local filter costs after remote sorts in postgres_fdw Local filters run after a remote sort, and LIMIT can stop them early. Moving their per-tuple cost into the sort startup cost can instead select a local sort that evaluates the filter on every row. Exclude the local filter cost while adding the remote sort, then restore it to run_cost. Use the same row count as the underlying estimate so the total filter cost is preserved. --- .../postgres_fdw/expected/postgres_fdw.out | 61 +++++++++++++++++++ contrib/postgres_fdw/postgres_fdw.c | 14 +++++ contrib/postgres_fdw/sql/postgres_fdw.sql | 28 +++++++++ 3 files changed, 103 insertions(+) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index e087bf8260c..2c62cc7a1e4 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -254,6 +254,67 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again -- and remote-estimate mode on ft2. ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); +-- A remote sort must not charge local filtering before returning any rows. +SET work_mem = '4MB'; +CREATE TABLE sort_src (id int, payload text); +INSERT INTO sort_src + SELECT i, repeat('x', 512) FROM generate_series(1, 20000) i; +CREATE FOREIGN TABLE sort_ft (id int, payload text) SERVER loopback + OPTIONS (table_name 'sort_src'); +ANALYZE sort_src; +ANALYZE sort_ft; +CREATE FUNCTION local_filter(int) RETURNS boolean +LANGUAGE plpgsql IMMUTABLE AS $$ +BEGIN + RETURN $1 > 0; +END +$$; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id, payload FROM sort_ft WHERE local_filter(id) ORDER BY id LIMIT 10; + QUERY PLAN +---------------------------------------------------------------------------------------- + Limit + Output: id, payload + -> Foreign Scan on public.sort_ft + Output: id, payload + Filter: local_filter(sort_ft.id) + Remote SQL: SELECT id, payload FROM public.sort_src ORDER BY id ASC NULLS LAST +(6 rows) + +SELECT id FROM sort_ft WHERE local_filter(id) ORDER BY id LIMIT 10; + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +-- The remote filter reduces retrieved_rows, but the base scan still charges +-- local qual costs on all tuples. Keep all of that cost after the sort. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM sort_ft WHERE id <= 100 AND local_filter(id) +ORDER BY id LIMIT 10; + QUERY PLAN +--------------------------------------------------------------------------------------------------- + Limit + Output: id + -> Foreign Scan on public.sort_ft + Output: id + Filter: local_filter(sort_ft.id) + Remote SQL: SELECT id FROM public.sort_src WHERE ((id <= 100)) ORDER BY id ASC NULLS LAST +(6 rows) + +DROP FUNCTION local_filter(int); +DROP FOREIGN TABLE sort_ft; +DROP TABLE sort_src; +RESET work_mem; -- =================================================================== -- test subscription -- =================================================================== diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 40f93fb6960..28291747bff 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -3802,10 +3802,24 @@ estimate_path_cost_size(PlannerInfo *root, * pushing down the ORDER BY clause when it's useful to do so. */ if (pathkeys != NIL) + { + Cost local_run_cost; + + /* + * Local quals run after the remote sort, so keep their per-tuple + * cost out of its startup cost. Match the row count used above: + * base scans charge quals on all tuples, joins and groups on + * retrieved rows. + */ + local_run_cost = fpinfo->local_conds_cost.per_tuple * + (IS_SIMPLE_REL(foreignrel) ? foreignrel->tuples : retrieved_rows); + run_cost -= local_run_cost; adjust_foreign_path_cost_for_sort(root, pathkeys, retrieved_rows, width, fpextra ? fpextra->limit_tuples : -1.0, &startup_cost, &run_cost); + run_cost += local_run_cost; + } total_cost = startup_cost + run_cost; diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index e858f740d6e..d5120b5c022 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -244,6 +244,34 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); +-- A remote sort must not charge local filtering before returning any rows. +SET work_mem = '4MB'; +CREATE TABLE sort_src (id int, payload text); +INSERT INTO sort_src + SELECT i, repeat('x', 512) FROM generate_series(1, 20000) i; +CREATE FOREIGN TABLE sort_ft (id int, payload text) SERVER loopback + OPTIONS (table_name 'sort_src'); +ANALYZE sort_src; +ANALYZE sort_ft; +CREATE FUNCTION local_filter(int) RETURNS boolean +LANGUAGE plpgsql IMMUTABLE AS $$ +BEGIN + RETURN $1 > 0; +END +$$; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id, payload FROM sort_ft WHERE local_filter(id) ORDER BY id LIMIT 10; +SELECT id FROM sort_ft WHERE local_filter(id) ORDER BY id LIMIT 10; +-- The remote filter reduces retrieved_rows, but the base scan still charges +-- local qual costs on all tuples. Keep all of that cost after the sort. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT id FROM sort_ft WHERE id <= 100 AND local_filter(id) +ORDER BY id LIMIT 10; +DROP FUNCTION local_filter(int); +DROP FOREIGN TABLE sort_ft; +DROP TABLE sort_src; +RESET work_mem; + -- =================================================================== -- test subscription -- =================================================================== -- 2.43.7