diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 0b9e3e4..3253eec 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -1505,6 +1505,35 @@ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 | 21 (10 rows) +-- full outer join + WHERE clause with shippable extension set +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.c1, t2.c2, t1.c3 FROM ft1 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE postgres_fdw_abs(t1.c1) > 0 OFFSET 10 LIMIT 10; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c2, t1.c3 + -> Foreign Scan + Output: t1.c1, t2.c2, t1.c3 + Relations: (public.ft1 t1) FULL JOIN (public.ft2 t2) + Remote SQL: SELECT r1."C 1", r1.c3, r2.c2 FROM ("S 1"."T 1" r1 FULL JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) WHERE ((public.postgres_fdw_abs(r1."C 1") > 0)) +(6 rows) + +ALTER SERVER loopback OPTIONS (DROP extensions); +-- full outer join + WHERE clause with shippable extension not set +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.c1, t2.c2, t1.c3 FROM ft1 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE postgres_fdw_abs(t1.c1) > 0 OFFSET 10 LIMIT 10; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c2, t1.c3 + -> Foreign Scan + Output: t1.c1, t2.c2, t1.c3 + Filter: (postgres_fdw_abs(t1.c1) > 0) + Relations: (public.ft1 t1) FULL JOIN (public.ft2 t2) + Remote SQL: SELECT r1."C 1", r1.c3, r2.c2 FROM ("S 1"."T 1" r1 FULL JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) +(7 rows) + +ALTER SERVER loopback OPTIONS (ADD extensions 'postgres_fdw'); -- join two tables with FOR UPDATE clause -- tests whole-row reference for row marks EXPLAIN (VERBOSE, COSTS OFF) diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 5d270b9..6df46bf 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -415,6 +415,11 @@ static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel, static void add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel, RelOptInfo *grouped_rel); +static void apply_server_options(PgFdwRelationInfo *fpinfo); +static void apply_table_options(PgFdwRelationInfo *fpinfo); +static void merge_fdw_options(PgFdwRelationInfo *fpinfo, + PgFdwRelationInfo *fpinfo_o, + PgFdwRelationInfo *fpinfo_i); /* @@ -514,31 +519,9 @@ postgresGetForeignRelSize(PlannerInfo *root, fpinfo->shippable_extensions = NIL; fpinfo->fetch_size = 100; - foreach(lc, fpinfo->server->options) - { - DefElem *def = (DefElem *) lfirst(lc); + apply_server_options(fpinfo); - if (strcmp(def->defname, "use_remote_estimate") == 0) - fpinfo->use_remote_estimate = defGetBoolean(def); - else if (strcmp(def->defname, "fdw_startup_cost") == 0) - fpinfo->fdw_startup_cost = strtod(defGetString(def), NULL); - else if (strcmp(def->defname, "fdw_tuple_cost") == 0) - fpinfo->fdw_tuple_cost = strtod(defGetString(def), NULL); - else if (strcmp(def->defname, "extensions") == 0) - fpinfo->shippable_extensions = - ExtractExtensionList(defGetString(def), false); - else if (strcmp(def->defname, "fetch_size") == 0) - fpinfo->fetch_size = strtol(defGetString(def), NULL, 10); - } - foreach(lc, fpinfo->table->options) - { - DefElem *def = (DefElem *) lfirst(lc); - - if (strcmp(def->defname, "use_remote_estimate") == 0) - fpinfo->use_remote_estimate = defGetBoolean(def); - else if (strcmp(def->defname, "fetch_size") == 0) - fpinfo->fetch_size = strtol(defGetString(def), NULL, 10); - } + apply_table_options(fpinfo); /* * If the table or the server is configured to use remote estimates, @@ -4090,6 +4073,12 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, joinclauses = NIL; } + fpinfo->outerrel = outerrel; + fpinfo->innerrel = innerrel; + fpinfo->jointype = jointype; + + merge_fdw_options(fpinfo, fpinfo_o, fpinfo_i); + /* Join quals must be safe to push down. */ foreach(lc, joinclauses) { @@ -4140,10 +4129,6 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, fpinfo->remote_conds = lappend(fpinfo->remote_conds, expr); } - fpinfo->outerrel = outerrel; - fpinfo->innerrel = innerrel; - fpinfo->jointype = jointype; - /* * Pull the other remote conditions from the joining relations into join * clauses or other remote clauses (remote_conds) of this relation @@ -4213,15 +4198,6 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, /* Mark that this join can be pushed down safely */ fpinfo->pushdown_safe = true; - /* - * If user is willing to estimate cost for a scan of either of the joining - * relations using EXPLAIN, he intends to estimate scans on that relation - * more accurately. Then, it makes sense to estimate the cost of the join - * with that relation more accurately using EXPLAIN. - */ - fpinfo->use_remote_estimate = fpinfo_o->use_remote_estimate || - fpinfo_i->use_remote_estimate; - /* Get user mapping */ if (fpinfo->use_remote_estimate) { @@ -4233,17 +4209,6 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, else fpinfo->user = NULL; - /* Get foreign server */ - fpinfo->server = fpinfo_o->server; - - /* - * Since both the joining relations come from the same server, the server - * level options should have same value for both the relations. Pick from - * any side. - */ - fpinfo->fdw_startup_cost = fpinfo_o->fdw_startup_cost; - fpinfo->fdw_tuple_cost = fpinfo_o->fdw_tuple_cost; - /* * Set cached relation costs to some negative value, so that we can detect * when they are set to some sensible costs, during one (usually the @@ -4253,15 +4218,6 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, fpinfo->rel_total_cost = -1; /* - * Set fetch size to maximum of the joining sides, since we are expecting - * the rows returned by the join to be proportional to the relation sizes. - */ - if (fpinfo_o->fetch_size > fpinfo_i->fetch_size) - fpinfo->fetch_size = fpinfo_o->fetch_size; - else - fpinfo->fetch_size = fpinfo_i->fetch_size; - - /* * Set the string describing this join relation to be used in EXPLAIN * output of corresponding ForeignScan. */ @@ -4309,6 +4265,114 @@ add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel, } /* + * apply_server_options + * Parse options from foreign server any apply them to 'fpinfo' + * + * New options may also require tweaking merge_fdw_options() + */ +static void +apply_server_options(PgFdwRelationInfo *fpinfo) +{ + ListCell *lc; + + foreach(lc, fpinfo->server->options) + { + DefElem *def = (DefElem *) lfirst(lc); + + if (strcmp(def->defname, "use_remote_estimate") == 0) + fpinfo->use_remote_estimate = defGetBoolean(def); + else if (strcmp(def->defname, "fdw_startup_cost") == 0) + fpinfo->fdw_startup_cost = strtod(defGetString(def), NULL); + else if (strcmp(def->defname, "fdw_tuple_cost") == 0) + fpinfo->fdw_tuple_cost = strtod(defGetString(def), NULL); + else if (strcmp(def->defname, "extensions") == 0) + fpinfo->shippable_extensions = + ExtractExtensionList(defGetString(def), false); + else if (strcmp(def->defname, "fetch_size") == 0) + fpinfo->fetch_size = strtol(defGetString(def), NULL, 10); + } +} + +/* + * apply_table_options + * Parse options from foreign table any apply them to 'fpinfo' + * + * New options may also require tweaking merge_fdw_options() + */ +static void +apply_table_options(PgFdwRelationInfo *fpinfo) +{ + ListCell *lc; + + foreach(lc, fpinfo->table->options) + { + DefElem *def = (DefElem *) lfirst(lc); + + if (strcmp(def->defname, "use_remote_estimate") == 0) + fpinfo->use_remote_estimate = defGetBoolean(def); + else if (strcmp(def->defname, "fetch_size") == 0) + fpinfo->fetch_size = strtol(defGetString(def), NULL, 10); + } +} + +/* + * merge_fdw_options + * Merge FDW options from into a new set of options for a join or an + * upper rel. + * + * For a join relation both the inner and outer relations are set. For an + * upper relation the input relation is outerrel, and innerrel is NULL. With a + * join rel, the foreign server on either side of the join is expected to + * match, this means we can derive server options from either side, although + * we choose outer. Some table level options may require merging. + */ +static void +merge_fdw_options(PgFdwRelationInfo *fpinfo, PgFdwRelationInfo *fpinfo_o, + PgFdwRelationInfo *fpinfo_i) +{ + /* We must always have fpinfo_o and it must always have an outerrel */ + Assert(fpinfo_o && fpinfo_o->outerrel); + + /* fpinfo_i may be NULL, but if present the servers must both match */ + Assert(!fpinfo_i || + fpinfo->innerrel->server->serverid == + fpinfo->outerrel->server->serverid); + + fpinfo->server = fpinfo_o->server; + + /* Copy the server specific FDW options from the outer relation. */ + fpinfo->fdw_startup_cost = fpinfo_o->fdw_startup_cost; + fpinfo->fdw_tuple_cost = fpinfo_o->fdw_tuple_cost; + fpinfo->shippable_extensions = fpinfo_o->shippable_extensions; + fpinfo->use_remote_estimate = fpinfo_o->use_remote_estimate; + fpinfo->fetch_size = fpinfo_o->fetch_size; + + /* + * When both rels infos are present, i.e. a join, we must merge the table + * level options from either side of the join. + */ + if (fpinfo_i) + { + /* + * We'll prefer to use remote estimates for this join if any table + * from either side of the join is using remote estimates. This is + * most likely going to be preferred since they're already willing to + * pay the price of a round trip to get the remote EXPLAIN. In any + * case it's not entirely clear how best else we might handle this. + */ + fpinfo->use_remote_estimate = fpinfo_o->use_remote_estimate || + fpinfo_i->use_remote_estimate; + + /* + * Set fetch size to maximum of the joining sides, since we are + * expecting the rows returned by the join to be proportional to the + * relation sizes. + */ + fpinfo->fetch_size = Max(fpinfo_o->fetch_size, fpinfo_i->fetch_size); + } +} + +/* * postgresGetForeignJoinPaths * Add possible ForeignPath to joinrel, if join is safe to push down. */ @@ -4617,18 +4681,6 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel) fpinfo->pushdown_safe = true; /* - * If user is willing to estimate cost for a scan using EXPLAIN, he - * intends to estimate scans on that relation more accurately. Then, it - * makes sense to estimate the cost of the grouping on that relation more - * accurately using EXPLAIN. - */ - fpinfo->use_remote_estimate = ofpinfo->use_remote_estimate; - - /* Copy startup and tuple cost as is from underneath input rel's fpinfo */ - fpinfo->fdw_startup_cost = ofpinfo->fdw_startup_cost; - fpinfo->fdw_tuple_cost = ofpinfo->fdw_tuple_cost; - - /* * Set cached relation costs to some negative value, so that we can detect * when they are set to some sensible costs, during one (usually the * first) of the calls to estimate_path_cost_size(). @@ -4636,9 +4688,6 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel) fpinfo->rel_startup_cost = -1; fpinfo->rel_total_cost = -1; - /* Set fetch size same as that of underneath input rel's fpinfo */ - fpinfo->fetch_size = ofpinfo->fetch_size; - /* * Set the string describing this grouped relation to be used in EXPLAIN * output of corresponding ForeignScan. @@ -4714,13 +4763,13 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel, fpinfo->outerrel = input_rel; /* - * Copy foreign table, foreign server, user mapping, shippable extensions - * etc. details from the input relation's fpinfo. + * Copy foreign table, foreign server, user mapping, FDW options etc. + * details from the input relation's fpinfo. */ fpinfo->table = ifpinfo->table; fpinfo->server = ifpinfo->server; fpinfo->user = ifpinfo->user; - fpinfo->shippable_extensions = ifpinfo->shippable_extensions; + merge_fdw_options(fpinfo, ifpinfo , NULL); /* Assess if it is safe to push down aggregation and grouping. */ if (!foreign_grouping_ok(root, grouped_rel)) diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 56b01d0..9403238 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -430,6 +430,14 @@ SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT EXPLAIN (VERBOSE, COSTS OFF) SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; +-- full outer join + WHERE clause with shippable extension set +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.c1, t2.c2, t1.c3 FROM ft1 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE postgres_fdw_abs(t1.c1) > 0 OFFSET 10 LIMIT 10; +ALTER SERVER loopback OPTIONS (DROP extensions); +-- full outer join + WHERE clause with shippable extension not set +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.c1, t2.c2, t1.c3 FROM ft1 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE postgres_fdw_abs(t1.c1) > 0 OFFSET 10 LIMIT 10; +ALTER SERVER loopback OPTIONS (ADD extensions 'postgres_fdw'); -- join two tables with FOR UPDATE clause -- tests whole-row reference for row marks EXPLAIN (VERBOSE, COSTS OFF)