"David E. Wheeler" <[email protected]> writes:
> On Jan 5, 2026, at 19:41, Tom Lane <[email protected]> wrote:
>> Yeah, it looks a little
>> odd to ship the query in this form rather than as a MIN()/MAX(),
>> but I'm unconvinced that it's harmful per se.

> It would be less harmful if the LIMIT pushed down; otherwise it’s pulling the 
> entire table back into Postgres, no?

Yeah, there might be some gold to be mined there.  IIRC we already
have some ability for LIMIT to tell an immediately-below Sort that
only a limited number of rows will be retrieved.  Could be useful
if ForeignScan could be in on that.

>> It would be good to look under the hood a little more and understand
>> why the MinMaxAgg path is winning, if it's not actually fast.

> Where does one start?

I made the attached test case (based on postgres_fdw.sql, but with
a few more rows in the table).  The idea is to check the behavior
for both an indexed column (c1) and an unindexed one (c2).  To
eliminate cacheing/startup effects, I ran each test case three times
but considered only the lowest execution time of the three (often
but not always the last run).

The first attached results are from current HEAD; the second are
after applying your patch.  What I find interesting is that
with use_remote_estimate enabled, it always goes for shipping the
MIN as-is.  Your patch changes the behavior with use_remote_estimate
disabled, and we can see that the reason is that that mode estimates
the query cost at 100.24 with the minmax optimization and 146.78 when
shipping the aggregate.  I've not dug into where we are getting
those numbers without use_remote_estimate, but perhaps that could
use refinement.  On the other hand, there is not that much difference
between the actual runtimes, so perhaps it doesn't matter for
postgres_fdw.

I can't speak to your results with ClickHouse, since I have no idea
how the cost estimates are derived for that.

                        regards, tom lane

CREATE EXTENSION postgres_fdw;

SELECT current_database() AS current_database,
  current_setting('port') AS current_port
\gset

CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw
        OPTIONS (dbname :'current_database', port :'current_port');
CREATE USER MAPPING FOR CURRENT_USER SERVER loopback;

CREATE SCHEMA "S 1";

CREATE TABLE "S 1"."T 3" (
        c1 int NOT NULL,
        c2 int NOT NULL,
        c3 text,
        CONSTRAINT t3_pkey PRIMARY KEY (c1)
);

INSERT INTO "S 1"."T 3"
        SELECT id,
               id + 1,
               'AAA' || to_char(id, 'FM000')
        FROM generate_series(1, 10000) id;

ANALYZE "S 1"."T 3";

CREATE FOREIGN TABLE ft4 (
        c1 int NOT NULL,
        c2 int NOT NULL,
        c3 text
) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 3');

explain (analyze, verbose) select min(c1) from "S 1"."T 3";
explain (analyze, verbose) select min(c1) from "S 1"."T 3";
explain (analyze, verbose) select min(c1) from "S 1"."T 3";
explain (analyze, verbose) select min(c2) from "S 1"."T 3";
explain (analyze, verbose) select min(c2) from "S 1"."T 3";
explain (analyze, verbose) select min(c2) from "S 1"."T 3";

ALTER FOREIGN TABLE ft4 OPTIONS (use_remote_estimate 'false');

explain (analyze, verbose) select min(c1) from ft4;
explain (analyze, verbose) select min(c1) from ft4;
explain (analyze, verbose) select min(c1) from ft4;
explain (analyze, verbose) select min(c2) from ft4;
explain (analyze, verbose) select min(c2) from ft4;
explain (analyze, verbose) select min(c2) from ft4;

ALTER FOREIGN TABLE ft4 OPTIONS (SET use_remote_estimate 'true');

explain (analyze, verbose) select min(c1) from ft4;
explain (analyze, verbose) select min(c1) from ft4;
explain (analyze, verbose) select min(c1) from ft4;
explain (analyze, verbose) select min(c2) from ft4;
explain (analyze, verbose) select min(c2) from ft4;
explain (analyze, verbose) select min(c2) from ft4;
                                                                   QUERY PLAN   
                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------------
 Result  (cost=0.32..0.33 rows=1 width=4) (actual time=0.012..0.012 rows=1.00 
loops=1)
   Output: (InitPlan minmax_1).col1
   Replaces: MinMaxAggregate
   Buffers: shared hit=3
   InitPlan minmax_1
     ->  Limit  (cost=0.29..0.32 rows=1 width=4) (actual time=0.011..0.011 
rows=1.00 loops=1)
           Output: "T 3".c1
           Buffers: shared hit=3
           ->  Index Only Scan using t3_pkey on "S 1"."T 3"  (cost=0.29..328.29 
rows=10000 width=4) (actual time=0.011..0.011 rows=1.00 loops=1)
                 Output: "T 3".c1
                 Heap Fetches: 1
                 Index Searches: 1
                 Buffers: shared hit=3
 Planning Time: 0.027 ms
 Execution Time: 0.019 ms
(15 rows)

                                                       QUERY PLAN               
                                        
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=180.00..180.01 rows=1 width=4) (actual time=1.057..1.058 
rows=1.00 loops=1)
   Output: min(c2)
   Buffers: shared hit=55
   ->  Seq Scan on "S 1"."T 3"  (cost=0.00..155.00 rows=10000 width=4) (actual 
time=0.004..0.414 rows=10000.00 loops=1)
         Output: c1, c2, c3
         Buffers: shared hit=55
 Planning Time: 0.026 ms
 Execution Time: 1.065 ms
(8 rows)

                                                           QUERY PLAN           
                                                
--------------------------------------------------------------------------------------------------------------------------------
 Result  (cost=100.23..100.24 rows=1 width=4) (actual time=0.145..0.145 
rows=1.00 loops=1)
   Output: (InitPlan minmax_1).col1
   Replaces: MinMaxAggregate
   InitPlan minmax_1
     ->  Limit  (cost=100.00..100.23 rows=1 width=4) (actual time=0.144..0.144 
rows=1.00 loops=1)
           Output: ft4.c1
           ->  Foreign Scan on public.ft4  (cost=100.00..761.35 rows=2925 
width=4) (actual time=0.144..0.144 rows=1.00 loops=1)
                 Output: ft4.c1
                 Remote SQL: SELECT c1 FROM "S 1"."T 3" ORDER BY c1 ASC NULLS 
LAST
 Planning Time: 0.030 ms
 Execution Time: 0.207 ms
(11 rows)

                                                           QUERY PLAN           
                                                
--------------------------------------------------------------------------------------------------------------------------------
 Result  (cost=100.23..100.24 rows=1 width=4) (actual time=0.832..0.832 
rows=1.00 loops=1)
   Output: (InitPlan minmax_1).col1
   Replaces: MinMaxAggregate
   InitPlan minmax_1
     ->  Limit  (cost=100.00..100.23 rows=1 width=4) (actual time=0.831..0.831 
rows=1.00 loops=1)
           Output: ft4.c2
           ->  Foreign Scan on public.ft4  (cost=100.00..761.35 rows=2925 
width=4) (actual time=0.831..0.831 rows=1.00 loops=1)
                 Output: ft4.c2
                 Remote SQL: SELECT c2 FROM "S 1"."T 3" ORDER BY c2 ASC NULLS 
LAST
 Planning Time: 0.029 ms
 Execution Time: 0.900 ms
(11 rows)

                                           QUERY PLAN                           
                 
-------------------------------------------------------------------------------------------------
 Foreign Scan  (cost=100.32..100.54 rows=1 width=4) (actual time=0.130..0.130 
rows=1.00 loops=1)
   Output: (min(c1))
   Relations: Aggregate on (public.ft4)
   Remote SQL: SELECT min(c1) FROM "S 1"."T 3"
 Planning Time: 0.315 ms
 Execution Time: 0.163 ms
(6 rows)

                                           QUERY PLAN                           
                 
-------------------------------------------------------------------------------------------------
 Foreign Scan  (cost=280.00..280.22 rows=1 width=4) (actual time=0.803..0.803 
rows=1.00 loops=1)
   Output: (min(c2))
   Relations: Aggregate on (public.ft4)
   Remote SQL: SELECT min(c2) FROM "S 1"."T 3"
 Planning Time: 0.316 ms
 Execution Time: 0.837 ms
(6 rows)
                                                                   QUERY PLAN   
                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------------
 Result  (cost=0.32..0.33 rows=1 width=4) (actual time=0.012..0.012 rows=1.00 
loops=1)
   Output: (InitPlan minmax_1).col1
   Replaces: MinMaxAggregate
   Buffers: shared hit=3
   InitPlan minmax_1
     ->  Limit  (cost=0.29..0.32 rows=1 width=4) (actual time=0.011..0.011 
rows=1.00 loops=1)
           Output: "T 3".c1
           Buffers: shared hit=3
           ->  Index Only Scan using t3_pkey on "S 1"."T 3"  (cost=0.29..328.29 
rows=10000 width=4) (actual time=0.011..0.011 rows=1.00 loops=1)
                 Output: "T 3".c1
                 Heap Fetches: 1
                 Index Searches: 1
                 Buffers: shared hit=3
 Planning Time: 0.026 ms
 Execution Time: 0.019 ms
(15 rows)

                                                       QUERY PLAN               
                                        
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=180.00..180.01 rows=1 width=4) (actual time=1.054..1.054 
rows=1.00 loops=1)
   Output: min(c2)
   Buffers: shared hit=55
   ->  Seq Scan on "S 1"."T 3"  (cost=0.00..155.00 rows=10000 width=4) (actual 
time=0.004..0.412 rows=10000.00 loops=1)
         Output: c1, c2, c3
         Buffers: shared hit=55
 Planning Time: 0.026 ms
 Execution Time: 1.061 ms
(8 rows)

                                           QUERY PLAN                           
                 
-------------------------------------------------------------------------------------------------
 Foreign Scan  (cost=107.31..146.78 rows=1 width=4) (actual time=0.127..0.127 
rows=1.00 loops=1)
   Output: (min(c1))
   Relations: Aggregate on (public.ft4)
   Remote SQL: SELECT min(c1) FROM "S 1"."T 3"
 Planning Time: 0.017 ms
 Execution Time: 0.187 ms
(6 rows)

                                           QUERY PLAN                           
                 
-------------------------------------------------------------------------------------------------
 Foreign Scan  (cost=107.31..146.78 rows=1 width=4) (actual time=0.795..0.796 
rows=1.00 loops=1)
   Output: (min(c2))
   Relations: Aggregate on (public.ft4)
   Remote SQL: SELECT min(c2) FROM "S 1"."T 3"
 Planning Time: 0.017 ms
 Execution Time: 0.859 ms
(6 rows)

                                           QUERY PLAN                           
                 
-------------------------------------------------------------------------------------------------
 Foreign Scan  (cost=100.32..100.54 rows=1 width=4) (actual time=0.124..0.124 
rows=1.00 loops=1)
   Output: (min(c1))
   Relations: Aggregate on (public.ft4)
   Remote SQL: SELECT min(c1) FROM "S 1"."T 3"
 Planning Time: 0.186 ms
 Execution Time: 0.157 ms
(6 rows)

                                           QUERY PLAN                           
                 
-------------------------------------------------------------------------------------------------
 Foreign Scan  (cost=280.00..280.22 rows=1 width=4) (actual time=0.801..0.802 
rows=1.00 loops=1)
   Output: (min(c2))
   Relations: Aggregate on (public.ft4)
   Remote SQL: SELECT min(c2) FROM "S 1"."T 3"
 Planning Time: 0.209 ms
 Execution Time: 0.836 ms
(6 rows)

Reply via email to