public inbox for [email protected]
help / color / mirror / Atom feedRe: Add semi-join pushdown to postgres_fdw
19+ messages / 6 participants
[nested] [flat]
* Re: Add semi-join pushdown to postgres_fdw
@ 2023-01-19 17:49 Tomas Vondra <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Tomas Vondra @ 2023-01-19 17:49 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; [email protected] <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>
Hi.
I took a quick look at the patch. It needs a rebase, although it applies
fine using patch.
A couple minor comments:
1) addl_conds seems a bit hard to understand, I'd use either the full
wording (additional_conds) or maybe extra_conds
2) some of the lines got quite long, and need a wrap
3) unknown_subquery_rels name is a bit misleading - AFAIK it's the rels
that can't be referenced from upper rels (per what the .h says). So they
are known, but hidden. Is there a better name?
4) joinrel_target_ok() needs a better comment, explaining *when* the
reltarget is safe for pushdown. The conditions are on the same row, but
the project style is to break after '&&'.
Also, I'd write
if (!IsA(var, Var))
continue;
which saves one level of nesting. IMHO that makes it more readable.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2023-01-20 09:00 Alexander Pyhalov <[email protected]>
parent: Tomas Vondra <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Pyhalov @ 2023-01-20 09:00 UTC (permalink / raw)
To: Tomas Vondra <[email protected]>; +Cc: [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>
Hi.
Tomas Vondra писал 2023-01-19 20:49:
> I took a quick look at the patch. It needs a rebase, although it
> applies
> fine using patch.
>
> A couple minor comments:
>
> 1) addl_conds seems a bit hard to understand, I'd use either the full
> wording (additional_conds) or maybe extra_conds
Renamed to additional_conds.
>
> 2) some of the lines got quite long, and need a wrap
Splitted some of them. Not sure if it's enough.
>
> 3) unknown_subquery_rels name is a bit misleading - AFAIK it's the rels
> that can't be referenced from upper rels (per what the .h says). So
> they
> are known, but hidden. Is there a better name?
Renamed to hidden_subquery_rels. These are rels, which can't be referred
to from upper join levels.
>
> 4) joinrel_target_ok() needs a better comment, explaining *when* the
> reltarget is safe for pushdown. The conditions are on the same row, but
> the project style is to break after '&&'.
Added comment. It seems to be a rephrasing of lower comment in
joinrel_target_ok().
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] v4-0001-postgres_fdw-add-support-for-deparsing-semi-joins.patch (49.7K, ../../[email protected]/2-v4-0001-postgres_fdw-add-support-for-deparsing-semi-joins.patch)
download | inline diff:
From f37d26d9b622767f94e89034fa8e4fccc69e358d Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Mon, 7 Nov 2022 10:23:32 +0300
Subject: [PATCH v4] postgres_fdw: add support for deparsing semi joins
We deparse semi-joins as EXISTS subqueries. So, deparsing
semi-join leads to generating addl_conds condition,
which is then added to the uppermost JOIN's WHERE clause.
---
contrib/postgres_fdw/deparse.c | 206 +++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 297 ++++++++++++++++--
contrib/postgres_fdw/postgres_fdw.c | 89 +++++-
contrib/postgres_fdw/postgres_fdw.h | 2 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 119 ++++++-
5 files changed, 632 insertions(+), 81 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 473fa45bd43..1217d47050b 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -180,11 +180,14 @@ static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
Index ignore_rel, List **ignore_conds,
+ StringInfo additional_conds,
List **params_list);
+static void appendWhereClause(List *exprs, StringInfo additional_conds, deparse_expr_cxt *context);
static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool make_subquery,
- Index ignore_rel, List **ignore_conds, List **params_list);
+ Index ignore_rel, List **ignore_conds,
+ StringInfo additional_conds, List **params_list);
static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
static void appendOrderBySuffix(Oid sortop, Oid sortcoltype, bool nulls_first,
@@ -1370,23 +1373,21 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
{
StringInfo buf = context->buf;
RelOptInfo *scanrel = context->scanrel;
+ StringInfoData additional_conds;
/* For upper relations, scanrel must be either a joinrel or a baserel */
Assert(!IS_UPPER_REL(context->foreignrel) ||
IS_JOIN_REL(scanrel) || IS_SIMPLE_REL(scanrel));
+ initStringInfo(&additional_conds);
/* Construct FROM clause */
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, context->root, scanrel,
(bms_membership(scanrel->relids) == BMS_MULTIPLE),
- (Index) 0, NULL, context->params_list);
-
- /* Construct WHERE clause */
- if (quals != NIL)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(quals, context);
- }
+ (Index) 0, NULL, &additional_conds,
+ context->params_list);
+ appendWhereClause(quals, &additional_conds, context);
+ pfree(additional_conds.data);
}
/*
@@ -1598,6 +1599,33 @@ appendConditions(List *exprs, deparse_expr_cxt *context)
reset_transmission_modes(nestlevel);
}
+/*
+ * Append WHERE clause, containing conditions
+ * from exprs and additional_conds, to context->buf.
+ */
+static void
+appendWhereClause(List *exprs, StringInfo additional_conds, deparse_expr_cxt *context)
+{
+ StringInfo buf = context->buf;
+ bool need_and = false;
+
+ if (exprs != NIL || additional_conds->len > 0)
+ appendStringInfoString(buf, " WHERE ");
+
+ if (exprs != NIL)
+ {
+ appendConditions(exprs, context);
+ need_and = true;
+ }
+
+ if (additional_conds->len > 0)
+ {
+ if (need_and)
+ appendStringInfoString(buf, " AND ");
+ appendStringInfo(buf, "(%s)", additional_conds->data);
+ }
+}
+
/* Output join name for given join type */
const char *
get_jointype_name(JoinType jointype)
@@ -1616,6 +1644,9 @@ get_jointype_name(JoinType jointype)
case JOIN_FULL:
return "FULL";
+ case JOIN_SEMI:
+ return "SEMI";
+
default:
/* Shouldn't come here, but protect from buggy code. */
elog(ERROR, "unsupported join type %d", jointype);
@@ -1712,11 +1743,14 @@ deparseSubqueryTargetList(deparse_expr_cxt *context)
* of DELETE; it deparses the join relation as if the relation never contained
* the target relation, and creates a List of conditions to be deparsed into
* the top-level WHERE clause, which is returned to *ignore_conds.
+ *
+ * 'additional_conds' is a initialized StringInfo. If some additional conditions
+ * should be appended to WHERE clause, they are returned as additional_conds.
*/
static void
deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool use_alias, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ StringInfo additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1728,6 +1762,9 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
RelOptInfo *innerrel = fpinfo->innerrel;
bool outerrel_is_target = false;
bool innerrel_is_target = false;
+ StringInfoData additional_conds_i;
+ StringInfoData additional_conds_o;
+ bool need_and;
if (ignore_rel > 0 && bms_is_member(ignore_rel, foreignrel->relids))
{
@@ -1761,10 +1798,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse outer relation if not the target relation. */
if (!outerrel_is_target)
{
+ initStringInfo(&additional_conds_o);
initStringInfo(&join_sql_o);
deparseRangeTblRef(&join_sql_o, root, outerrel,
fpinfo->make_outerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_o,
+ params_list);
/*
* If inner relation is the target relation, skip deparsing it.
@@ -1780,6 +1819,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_o.data, join_sql_o.len);
+ if (additional_conds_o.len > 0)
+ {
+ Assert(additional_conds->len == 0);
+ appendStringInfoString(additional_conds, additional_conds_o.data);
+ }
+ pfree(additional_conds_o.data);
return;
}
}
@@ -1787,10 +1832,35 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse inner relation if not the target relation. */
if (!innerrel_is_target)
{
+ initStringInfo(&additional_conds_i);
+
initStringInfo(&join_sql_i);
deparseRangeTblRef(&join_sql_i, root, innerrel,
fpinfo->make_innerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_i,
+ params_list);
+
+ if (fpinfo->jointype == JOIN_SEMI)
+ {
+ deparse_expr_cxt context;
+
+ appendStringInfo(additional_conds, "EXISTS (SELECT NULL FROM %s",
+ join_sql_i.data);
+
+ context.buf = additional_conds;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
+
+ appendWhereClause(fpinfo->joinclauses, &additional_conds_i, &context);
+
+ if (additional_conds_i.len > 0)
+ resetStringInfo(&additional_conds_i);
+
+ /* Close parentheses for EXISTS subquery */
+ appendStringInfo(additional_conds, ")");
+ }
/*
* If outer relation is the target relation, skip deparsing it.
@@ -1801,6 +1871,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_i.data, join_sql_i.len);
+ if (additional_conds_i.len > 0)
+ {
+ Assert(additional_conds->len == 0);
+ appendStringInfoString(additional_conds, additional_conds_i.data);
+ }
+ pfree(additional_conds_i.data);
return;
}
}
@@ -1809,33 +1885,69 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(!outerrel_is_target && !innerrel_is_target);
/*
- * For a join relation FROM clause entry is deparsed as
- *
- * ((outer relation) <join type> (inner relation) ON (joinclauses))
+ * For semijoin FROM clause is deparsed as an outer relation. An inner
+ * relation and join clauses are converted to EXISTS condition and
+ * passed to the upper level.
*/
- appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
- get_jointype_name(fpinfo->jointype), join_sql_i.data);
-
- /* Append join clause; (TRUE) if no join clause */
- if (fpinfo->joinclauses)
+ if (fpinfo->jointype == JOIN_SEMI)
{
- deparse_expr_cxt context;
+ appendStringInfo(buf, "%s", join_sql_o.data);
+ }
+ else
+ {
+ /*
+ * For a join relation FROM clause entry is deparsed as
+ *
+ * ((outer relation) <join type> (inner relation) ON
+ * (joinclauses))
+ */
+ appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
+ get_jointype_name(fpinfo->jointype), join_sql_i.data);
+
+ /* Append join clause; (TRUE) if no join clause */
+ if (fpinfo->joinclauses)
+ {
+ deparse_expr_cxt context;
- context.buf = buf;
- context.foreignrel = foreignrel;
- context.scanrel = foreignrel;
- context.root = root;
- context.params_list = params_list;
+ context.buf = buf;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
- appendStringInfoChar(buf, '(');
- appendConditions(fpinfo->joinclauses, &context);
+ appendStringInfoChar(buf, '(');
+ appendConditions(fpinfo->joinclauses, &context);
+ appendStringInfoChar(buf, ')');
+ }
+ else
+ appendStringInfoString(buf, "(TRUE)");
+
+ /* End the FROM clause entry. */
appendStringInfoChar(buf, ')');
}
- else
- appendStringInfoString(buf, "(TRUE)");
- /* End the FROM clause entry. */
- appendStringInfoChar(buf, ')');
+ /*
+ * Construct additional_conds from current level additional_conds and
+ * additional_conds, coming from inner and outer rels.
+ */
+ need_and = false;
+ if (additional_conds->len > 0)
+ need_and = true;
+ if (additional_conds_o.len > 0)
+ {
+ if (need_and)
+ appendStringInfoString(additional_conds, " AND ");
+ appendStringInfoString(additional_conds, additional_conds_o.data);
+ need_and = true;
+ }
+ if (additional_conds_i.len > 0)
+ {
+ if (need_and)
+ appendStringInfoString(additional_conds, " AND ");
+ appendStringInfoString(additional_conds, additional_conds_i.data);
+ }
+ pfree(additional_conds_i.data);
+ pfree(additional_conds_o.data);
}
else
{
@@ -1867,7 +1979,7 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
static void
deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ StringInfo additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1925,7 +2037,8 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
else
deparseFromExprForRel(buf, root, foreignrel, true, ignore_rel,
- ignore_conds, params_list);
+ ignore_conds, additional_conds,
+ params_list);
}
/*
@@ -2148,6 +2261,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
ListCell *lc,
*lc2;
+ StringInfoData additional_conds;
/* Set up context struct for recursion */
context.root = root;
@@ -2185,21 +2299,21 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
reset_transmission_modes(nestlevel);
+ initStringInfo(&additional_conds);
+
if (foreignrel->reloptkind == RELOPT_JOINREL)
{
List *ignore_conds = NIL;
+
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, &additional_conds, &context);
+ pfree(additional_conds.data);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
@@ -2255,6 +2369,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
List **retrieved_attrs)
{
deparse_expr_cxt context;
+ StringInfoData additional_conds;
/* Set up context struct for recursion */
context.root = root;
@@ -2268,21 +2383,20 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
if (foreignrel->reloptkind == RELOPT_JOINREL)
appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+ initStringInfo(&additional_conds);
+
if (foreignrel->reloptkind == RELOPT_JOINREL)
{
List *ignore_conds = NIL;
appendStringInfoString(buf, " USING ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, &additional_conds, &context);
+ pfree(additional_conds.data);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2350cfe1487..75a9acee522 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2000,23 +2000,16 @@ SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER B
Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END, r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
(4 rows)
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Limit
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1
- -> Merge Semi Join
- Output: t1.c1
- Merge Cond: (t1.c1 = t2.c1)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
- -> Foreign Scan on public.ft2 t2
- Output: t2.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
-(11 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1" FROM "S 1"."T 1" r1 WHERE (EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r2."C 1" = r1."C 1")))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
+(4 rows)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
c1
@@ -3996,23 +3989,13 @@ EXECUTE st2(101, 121);
-- subquery using immutable function (can be sent to remote)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c5) = '1970-01-17'::date) ORDER BY c1;
EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st3(10, 20);
- QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Sort Key: t1.c1
- -> Nested Loop Semi Join
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Join Filter: (t2.c3 = t1.c3)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 20))
- -> Materialize
- Output: t2.c3
- -> Foreign Scan on public.ft2 t2
- Output: t2.c3
- Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE (("C 1" > 10)) AND ((date(c5) = '1970-01-17'::date))
-(14 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" < 20)) AND (EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r3."C 1" > 10)) AND ((date(r3.c5) = '1970-01-17'::date)) AND ((r3.c3 = r1.c3)))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
@@ -4556,6 +4539,260 @@ explain (verbose, costs off) select * from ft3 f, loct3 l
Index Cond: (l.f1 = 'foo'::text)
(12 rows)
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r2.c1, r2.c2, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 900)))) WHERE (EXISTS (SELECT NULL FROM "S 1"."T 4" r4 WHERE ((r1.c2 = r4.c1)))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r4.c1, r4.c2, r4.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r4 ON (((r1.c2 = r4.c1)) AND ((r1."C 1" > 900)))) WHERE (EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1)))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: (public.ft2) LEFT JOIN ((public.ft4) SEMI JOIN (public.ft5))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, s6.c1, s6.c2, s6.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r4.c1, r4.c2, r4.c3 FROM "S 1"."T 3" r4 WHERE (EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1))))) s6(c1, c2, c3) ON (((r1.c2 = s6.c1)))) WHERE ((r1."C 1" > 900)) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 901 | 1 | 00901 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo | | |
+ 902 | 2 | 00902 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2 | foo | | |
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo | | |
+ 904 | 4 | 00904 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4 | foo | | |
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo | | |
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo | | |
+ 908 | 8 | 00908 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8 | foo | | |
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo | | |
+ 910 | 0 | 00910 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0 | foo | | |
+(10 rows)
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)) INNER JOIN (public.ft5 ft5_1)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 4" r8 ON (((r1.c2 <= r8.c1)))) WHERE (EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r8.c1 = r9.c1))) AND EXISTS (SELECT NULL FROM "S 1"."T 4" r7 WHERE ((r6.c1 = r7.c1)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+ Relations: (public.ft2) SEMI JOIN ((public.ft2 ft2_1) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND (EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r1."C 1" = r3."C 1")) AND (EXISTS (SELECT NULL FROM "S 1"."T 3" r4 WHERE ((r3.c2 = r4.c2)))))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ftupper.c1, ftupper.c2, ftupper.c3, ftupper.c4, ftupper.c5, ftupper.c6, ftupper.c7, ftupper.c8
+ Relations: (public.ft2 ftupper) SEMI JOIN ((public.ft2) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND (EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r1."C 1" = r2."C 1")) AND (EXISTS (SELECT NULL FROM "S 1"."T 3" r3 WHERE ((r2.c2 = r3.c2)))))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft2 ft2_1)) INNER JOIN (public.ft2 ft2_2)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 1" r8 ON (((r1.c2 = r8.c2)))) WHERE (EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r1.c2 = r9.c2))) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r7 WHERE ((r7.c2 = r6.c2)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+----+----+----+----+----+----+----+----+----+----+----
+(0 rows)
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 50d23f922cc..de08a0e20f2 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -779,6 +779,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
fpinfo->make_outerrel_subquery = false;
fpinfo->make_innerrel_subquery = false;
fpinfo->lower_subquery_rels = NULL;
+ fpinfo->hidden_subquery_rels = NULL;
/* Set the relation index. */
fpinfo->relation_index = baserel->relid;
}
@@ -5723,6 +5724,46 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
return commands;
}
+/*
+ * Check if reltarget is safe enough to push down such join.
+ * For semi-join reltarget is not safe, if it contains references
+ * to inner rel relids, which do not belong to outer rel.
+ */
+static bool
+joinrel_target_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, RelOptInfo *outerrel, RelOptInfo *innerrel)
+{
+ List *vars;
+ ListCell *lc;
+ bool ok = true;
+
+ Assert(joinrel->reltarget);
+
+ vars = pull_var_clause((Node *) joinrel->reltarget->exprs, PVC_INCLUDE_PLACEHOLDERS);
+
+ foreach(lc, vars)
+ {
+ Var *var = (Var *) lfirst(lc);
+
+ if (!IsA(var, Var))
+ continue;
+
+ if (jointype == JOIN_SEMI &&
+ bms_is_member(var->varno, innerrel->relids) &&
+ !bms_is_member(var->varno, outerrel->relids))
+ {
+ /*
+ * Planner can create semi-joins, which refer to inner rel vars in
+ * its target list. However, we deparse semi-join as exists()
+ * subquery, so can't handle references to inner rel in target
+ * list.
+ */
+ ok = false;
+ break;
+ }
+ }
+ return ok;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -5740,12 +5781,19 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
List *joinclauses;
/*
- * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
- * Constructing queries representing SEMI and ANTI joins is hard, hence
- * not considered right now.
+ * We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
+ * Constructing queries representing ANTI joins is hard, hence not
+ * considered right now.
*/
if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
- jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+ jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+ jointype != JOIN_SEMI)
+ return false;
+
+ /*
+ * We can't push down join if its reltarget is not safe
+ */
+ if (!joinrel_target_ok(root, joinrel, jointype, outerrel, innerrel))
return false;
/*
@@ -5857,6 +5905,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
fpinfo->lower_subquery_rels = bms_union(fpinfo_o->lower_subquery_rels,
fpinfo_i->lower_subquery_rels);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo_o->hidden_subquery_rels,
+ fpinfo_i->hidden_subquery_rels);
/*
* Pull the other remote conditions from the joining relations into join
@@ -5900,6 +5950,16 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo_i->remote_conds);
break;
+ case JOIN_SEMI:
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo_i->remote_conds);
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo->remote_conds);
+ fpinfo->remote_conds = list_copy(fpinfo_o->remote_conds);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo->hidden_subquery_rels,
+ innerrel->relids);
+ break;
+
case JOIN_FULL:
/*
@@ -5942,6 +6002,24 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo->joinclauses = fpinfo->remote_conds;
fpinfo->remote_conds = NIL;
}
+ else if (jointype == JOIN_LEFT || jointype == JOIN_RIGHT || jointype == JOIN_FULL)
+ {
+ /*
+ * Conditions, generated from semi-joins, should be evaluated before
+ * LEFT/RIGHT/FULL join.
+ */
+ if (!bms_is_empty(fpinfo_o->hidden_subquery_rels))
+ {
+ fpinfo->make_outerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, outerrel->relids);
+ }
+
+ if (!bms_is_empty(fpinfo_i->hidden_subquery_rels))
+ {
+ fpinfo->make_innerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, innerrel->relids);
+ }
+ }
/* Mark that this join can be pushed down safely */
fpinfo->pushdown_safe = true;
@@ -7679,6 +7757,8 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
{
ListCell *lc;
+ PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
+
foreach(lc, ec->ec_members)
{
EquivalenceMember *em = (EquivalenceMember *) lfirst(lc);
@@ -7689,6 +7769,7 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
*/
if (bms_is_subset(em->em_relids, rel->relids) &&
!bms_is_empty(em->em_relids) &&
+ bms_is_empty(bms_intersect(em->em_relids, fpinfo->hidden_subquery_rels)) &&
is_foreign_expr(root, rel, em->em_expr))
return em;
}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 02c11523199..2b6d54a8fd2 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -118,6 +118,8 @@ typedef struct PgFdwRelationInfo
* subquery? */
Relids lower_subquery_rels; /* all relids appearing in lower
* subqueries */
+ Relids hidden_subquery_rels; /* relids, which can't be referred to
+ * from upper relations */
/*
* Index of the relation. It is used to create an alias to a subquery
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index c37aa803836..e28ea7cd853 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -599,7 +599,7 @@ WITH t (c1_1, c1_3, c2_1) AS MATERIALIZED (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t
-- ctid with whole-row reference
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
@@ -1274,6 +1274,123 @@ explain (verbose, costs off) select * from ft3 where f2 = 'foo' COLLATE "C";
explain (verbose, costs off) select * from ft3 f, loct3 l
where f.f3 = l.f3 COLLATE "POSIX" and l.f1 = 'foo';
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
--
2.34.1
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2023-10-30 16:05 Alexander Korotkov <[email protected]>
parent: Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Korotkov @ 2023-10-30 16:05 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>
Hi, Alexander!
Thank you for working on this. I believe this is a very interesting patch,
which significantly improves our FDW-based distributed facilities. This is
why I decided to review this.
On Fri, Jan 20, 2023 at 11:00 AM Alexander Pyhalov <[email protected]>
wrote:
> Tomas Vondra писал 2023-01-19 20:49:
> > I took a quick look at the patch. It needs a rebase, although it
> > applies
> > fine using patch.
> >
> > A couple minor comments:
> >
> > 1) addl_conds seems a bit hard to understand, I'd use either the full
> > wording (additional_conds) or maybe extra_conds
>
> Renamed to additional_conds.
>
> >
> > 2) some of the lines got quite long, and need a wrap
> Splitted some of them. Not sure if it's enough.
>
> >
> > 3) unknown_subquery_rels name is a bit misleading - AFAIK it's the rels
> > that can't be referenced from upper rels (per what the .h says). So
> > they
> > are known, but hidden. Is there a better name?
>
> Renamed to hidden_subquery_rels. These are rels, which can't be referred
> to from upper join levels.
>
> >
> > 4) joinrel_target_ok() needs a better comment, explaining *when* the
> > reltarget is safe for pushdown. The conditions are on the same row, but
> > the project style is to break after '&&'.
>
> Added comment. It seems to be a rephrasing of lower comment in
> joinrel_target_ok().
+ /*
+ * We can't push down join if its reltarget is not safe
+ */
+ if (!joinrel_target_ok(root, joinrel, jointype, outerrel, innerrel))
return false;
As I get joinrel_target_ok() function do meaningful checks only for semi
join and always return false for all other kinds of joins. I think we
should call this only for semi join and name the function accordingly.
+ fpinfo->unknown_subquery_rels =
bms_union(fpinfo_o->unknown_subquery_rels,
+
fpinfo_i->unknown_subquery_rels);
Should the comment before this code block be revised?
+ case JOIN_SEMI:
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo_i->remote_conds);
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo->remote_conds);
+ fpinfo->remote_conds = list_copy(fpinfo_o->remote_conds);
+ fpinfo->unknown_subquery_rels =
bms_union(fpinfo->unknown_subquery_rels,
+ innerrel->relids);
+ break;
I think that comment before switch() should be definitely revised.
+ Relids hidden_subquery_rels; /* relids, which can't be referred to
+ * from upper relations */
Could this definition contain the positive part? Can't be referred to from
upper relations, but used internally for semi joins (or something like
that)?
Also, I think the machinery around the append_conds could be somewhat
simpler if we turn them into a list (list of strings). I think that should
make code clearer and also save us some memory allocations.
In [1] you've referenced the cases, when your patch can't push down
semi-joins. It doesn't seem impossible to handle these cases, but that
would make the patch much more complicated. I'm OK to continue with a
simpler patch to handle the majority of cases. Could you please add the
cases, which can't be pushed down with the current patch, to the test suite?
Links
1.
https://www.postgresql.org/message-id/816fa8b1bc2da09a87484d1ef239a332%40postgrespro.ru
------
Regards,
Alexander Korotkov
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2023-10-31 11:07 Alexander Pyhalov <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Pyhalov @ 2023-10-31 11:07 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>
Alexander Korotkov писал 2023-10-30 19:05:
> Hi, Alexander!
>
> Thank you for working on this. I believe this is a very interesting
> patch, which significantly improves our FDW-based distributed
> facilities. This is why I decided to review this.
>
Hi. Thanks for reviewing.
> + /*
> + * We can't push down join if its reltarget is not safe
> + */
> + if (!joinrel_target_ok(root, joinrel, jointype, outerrel,
> innerrel))
> return false;
>
> As I get joinrel_target_ok() function do meaningful checks only for
> semi join and always return false for all other kinds of joins. I
> think we should call this only for semi join and name the function
> accordingly.
Done.
>
> + fpinfo->unknown_subquery_rels =
> bms_union(fpinfo_o->unknown_subquery_rels,
> +
> fpinfo_i->unknown_subquery_rels);
>
> Should the comment before this code block be revised?
Updated comment.
>
> + case JOIN_SEMI:
> + fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
> + fpinfo_i->remote_conds);
> + fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
> + fpinfo->remote_conds);
> + fpinfo->remote_conds = list_copy(fpinfo_o->remote_conds);
> + fpinfo->unknown_subquery_rels =
> bms_union(fpinfo->unknown_subquery_rels,
> + innerrel->relids);
> + break;
>
> I think that comment before switch() should be definitely revised.
>
> + Relids hidden_subquery_rels; /* relids, which can't be referred to
> + * from upper relations */
>
> Could this definition contain the positive part? Can't be referred to
> from upper relations, but used internally for semi joins (or something
> like that)?
Made comment a bit more verbose.
>
> Also, I think the machinery around the append_conds could be somewhat
> simpler if we turn them into a list (list of strings). I think that
> should make code clearer and also save us some memory allocations.
>
I've tried to rewrite it as managing lists.. to find out that these are
not lists.
I mean, in deparseFromExprForRel() we replace lists from both side with
one condition.
This allows us to preserve conditions hierarchy. We should merge these
conditions
in the end of IS_JOIN_REL(foreignrel) branch, or we'll push them too
high. And if we
deparse them in this place as StringInfo, I see no benefit to convert
them to lists.
> In [1] you've referenced the cases, when your patch can't push down
> semi-joins. It doesn't seem impossible to handle these cases, but
> that would make the patch much more complicated. I'm OK to continue
> with a simpler patch to handle the majority of cases. Could you
> please add the cases, which can't be pushed down with the current
> patch, to the test suite?
>
There are several cases when we can't push down semi-join in current
patch.
1) When target list has attributes from inner relation, which are
equivalent to some attributes of outer
relation, we fail to notice this.
2) When we examine A join B and decide that we can't push it down, this
decision is final - we state it in fdw_private of joinrel,
and so if we consider joining these relations in another order, we don't
reconsider.
This means that if later examine B join A, we don't try to push it down.
As semi-join can be executed as JOIN_UNIQUE_INNER or JOIN_UNIQUE_OUTER,
this can be a problem - we look at some of these paths and remember that
we can't push down such join.
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] v5-0001-postgres_fdw-add-support-for-deparsing-semi-joins.patch (51.9K, ../../[email protected]/2-v5-0001-postgres_fdw-add-support-for-deparsing-semi-joins.patch)
download | inline diff:
From 91ae85ac735c9f109cd3ab3603693177011e5b94 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Mon, 7 Nov 2022 10:23:32 +0300
Subject: [PATCH] postgres_fdw: add support for deparsing semi joins
We deparse semi-joins as EXISTS subqueries. So, deparsing
semi-join leads to generating addl_conds condition,
which is then added to the uppermost JOIN's WHERE clause.
---
contrib/postgres_fdw/deparse.c | 206 ++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 320 ++++++++++++++++--
contrib/postgres_fdw/postgres_fdw.c | 94 ++++-
contrib/postgres_fdw/postgres_fdw.h | 3 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 126 ++++++-
5 files changed, 668 insertions(+), 81 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 09fd489a901..cb0e373055d 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -180,11 +180,14 @@ static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
Index ignore_rel, List **ignore_conds,
+ StringInfo additional_conds,
List **params_list);
+static void appendWhereClause(List *exprs, StringInfo additional_conds, deparse_expr_cxt *context);
static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool make_subquery,
- Index ignore_rel, List **ignore_conds, List **params_list);
+ Index ignore_rel, List **ignore_conds,
+ StringInfo additional_conds, List **params_list);
static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
static void appendOrderBySuffix(Oid sortop, Oid sortcoltype, bool nulls_first,
@@ -1370,23 +1373,21 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
{
StringInfo buf = context->buf;
RelOptInfo *scanrel = context->scanrel;
+ StringInfoData additional_conds;
/* For upper relations, scanrel must be either a joinrel or a baserel */
Assert(!IS_UPPER_REL(context->foreignrel) ||
IS_JOIN_REL(scanrel) || IS_SIMPLE_REL(scanrel));
+ initStringInfo(&additional_conds);
/* Construct FROM clause */
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, context->root, scanrel,
(bms_membership(scanrel->relids) == BMS_MULTIPLE),
- (Index) 0, NULL, context->params_list);
-
- /* Construct WHERE clause */
- if (quals != NIL)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(quals, context);
- }
+ (Index) 0, NULL, &additional_conds,
+ context->params_list);
+ appendWhereClause(quals, &additional_conds, context);
+ pfree(additional_conds.data);
}
/*
@@ -1598,6 +1599,33 @@ appendConditions(List *exprs, deparse_expr_cxt *context)
reset_transmission_modes(nestlevel);
}
+/*
+ * Append WHERE clause, containing conditions
+ * from exprs and additional_conds, to context->buf.
+ */
+static void
+appendWhereClause(List *exprs, StringInfo additional_conds, deparse_expr_cxt *context)
+{
+ StringInfo buf = context->buf;
+ bool need_and = false;
+
+ if (exprs != NIL || additional_conds->len > 0)
+ appendStringInfoString(buf, " WHERE ");
+
+ if (exprs != NIL)
+ {
+ appendConditions(exprs, context);
+ need_and = true;
+ }
+
+ if (additional_conds->len > 0)
+ {
+ if (need_and)
+ appendStringInfoString(buf, " AND ");
+ appendStringInfo(buf, "(%s)", additional_conds->data);
+ }
+}
+
/* Output join name for given join type */
const char *
get_jointype_name(JoinType jointype)
@@ -1616,6 +1644,9 @@ get_jointype_name(JoinType jointype)
case JOIN_FULL:
return "FULL";
+ case JOIN_SEMI:
+ return "SEMI";
+
default:
/* Shouldn't come here, but protect from buggy code. */
elog(ERROR, "unsupported join type %d", jointype);
@@ -1712,11 +1743,14 @@ deparseSubqueryTargetList(deparse_expr_cxt *context)
* of DELETE; it deparses the join relation as if the relation never contained
* the target relation, and creates a List of conditions to be deparsed into
* the top-level WHERE clause, which is returned to *ignore_conds.
+ *
+ * 'additional_conds' is a initialized StringInfo. If some additional conditions
+ * should be appended to WHERE clause, they are returned as additional_conds.
*/
static void
deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool use_alias, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ StringInfo additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1728,6 +1762,9 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
RelOptInfo *innerrel = fpinfo->innerrel;
bool outerrel_is_target = false;
bool innerrel_is_target = false;
+ StringInfoData additional_conds_i;
+ StringInfoData additional_conds_o;
+ bool need_and;
if (ignore_rel > 0 && bms_is_member(ignore_rel, foreignrel->relids))
{
@@ -1761,10 +1798,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse outer relation if not the target relation. */
if (!outerrel_is_target)
{
+ initStringInfo(&additional_conds_o);
initStringInfo(&join_sql_o);
deparseRangeTblRef(&join_sql_o, root, outerrel,
fpinfo->make_outerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_o,
+ params_list);
/*
* If inner relation is the target relation, skip deparsing it.
@@ -1780,6 +1819,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_o.data, join_sql_o.len);
+ if (additional_conds_o.len > 0)
+ {
+ Assert(additional_conds->len == 0);
+ appendStringInfoString(additional_conds, additional_conds_o.data);
+ }
+ pfree(additional_conds_o.data);
return;
}
}
@@ -1787,10 +1832,35 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/* Deparse inner relation if not the target relation. */
if (!innerrel_is_target)
{
+ initStringInfo(&additional_conds_i);
+
initStringInfo(&join_sql_i);
deparseRangeTblRef(&join_sql_i, root, innerrel,
fpinfo->make_innerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_i,
+ params_list);
+
+ if (fpinfo->jointype == JOIN_SEMI)
+ {
+ deparse_expr_cxt context;
+
+ appendStringInfo(additional_conds, "EXISTS (SELECT NULL FROM %s",
+ join_sql_i.data);
+
+ context.buf = additional_conds;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
+
+ appendWhereClause(fpinfo->joinclauses, &additional_conds_i, &context);
+
+ if (additional_conds_i.len > 0)
+ resetStringInfo(&additional_conds_i);
+
+ /* Close parentheses for EXISTS subquery */
+ appendStringInfo(additional_conds, ")");
+ }
/*
* If outer relation is the target relation, skip deparsing it.
@@ -1801,6 +1871,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_i.data, join_sql_i.len);
+ if (additional_conds_i.len > 0)
+ {
+ Assert(additional_conds->len == 0);
+ appendStringInfoString(additional_conds, additional_conds_i.data);
+ }
+ pfree(additional_conds_i.data);
return;
}
}
@@ -1809,33 +1885,69 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(!outerrel_is_target && !innerrel_is_target);
/*
- * For a join relation FROM clause entry is deparsed as
- *
- * ((outer relation) <join type> (inner relation) ON (joinclauses))
+ * For semijoin FROM clause is deparsed as an outer relation. An inner
+ * relation and join clauses are converted to EXISTS condition and
+ * passed to the upper level.
*/
- appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
- get_jointype_name(fpinfo->jointype), join_sql_i.data);
-
- /* Append join clause; (TRUE) if no join clause */
- if (fpinfo->joinclauses)
+ if (fpinfo->jointype == JOIN_SEMI)
{
- deparse_expr_cxt context;
+ appendStringInfo(buf, "%s", join_sql_o.data);
+ }
+ else
+ {
+ /*
+ * For a join relation FROM clause entry is deparsed as
+ *
+ * ((outer relation) <join type> (inner relation) ON
+ * (joinclauses))
+ */
+ appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
+ get_jointype_name(fpinfo->jointype), join_sql_i.data);
+
+ /* Append join clause; (TRUE) if no join clause */
+ if (fpinfo->joinclauses)
+ {
+ deparse_expr_cxt context;
- context.buf = buf;
- context.foreignrel = foreignrel;
- context.scanrel = foreignrel;
- context.root = root;
- context.params_list = params_list;
+ context.buf = buf;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
- appendStringInfoChar(buf, '(');
- appendConditions(fpinfo->joinclauses, &context);
+ appendStringInfoChar(buf, '(');
+ appendConditions(fpinfo->joinclauses, &context);
+ appendStringInfoChar(buf, ')');
+ }
+ else
+ appendStringInfoString(buf, "(TRUE)");
+
+ /* End the FROM clause entry. */
appendStringInfoChar(buf, ')');
}
- else
- appendStringInfoString(buf, "(TRUE)");
- /* End the FROM clause entry. */
- appendStringInfoChar(buf, ')');
+ /*
+ * Construct additional_conds from current level additional_conds and
+ * additional_conds, coming from inner and outer rels.
+ */
+ need_and = false;
+ if (additional_conds->len > 0)
+ need_and = true;
+ if (additional_conds_o.len > 0)
+ {
+ if (need_and)
+ appendStringInfoString(additional_conds, " AND ");
+ appendStringInfoString(additional_conds, additional_conds_o.data);
+ need_and = true;
+ }
+ if (additional_conds_i.len > 0)
+ {
+ if (need_and)
+ appendStringInfoString(additional_conds, " AND ");
+ appendStringInfoString(additional_conds, additional_conds_i.data);
+ }
+ pfree(additional_conds_i.data);
+ pfree(additional_conds_o.data);
}
else
{
@@ -1867,7 +1979,7 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
static void
deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ StringInfo additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1925,7 +2037,8 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
else
deparseFromExprForRel(buf, root, foreignrel, true, ignore_rel,
- ignore_conds, params_list);
+ ignore_conds, additional_conds,
+ params_list);
}
/*
@@ -2148,6 +2261,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
ListCell *lc,
*lc2;
+ StringInfoData additional_conds;
/* Set up context struct for recursion */
context.root = root;
@@ -2185,21 +2299,21 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
reset_transmission_modes(nestlevel);
+ initStringInfo(&additional_conds);
+
if (foreignrel->reloptkind == RELOPT_JOINREL)
{
List *ignore_conds = NIL;
+
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, &additional_conds, &context);
+ pfree(additional_conds.data);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
@@ -2255,6 +2369,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
List **retrieved_attrs)
{
deparse_expr_cxt context;
+ StringInfoData additional_conds;
/* Set up context struct for recursion */
context.root = root;
@@ -2268,21 +2383,20 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
if (foreignrel->reloptkind == RELOPT_JOINREL)
appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+ initStringInfo(&additional_conds);
+
if (foreignrel->reloptkind == RELOPT_JOINREL)
{
List *ignore_conds = NIL;
appendStringInfoString(buf, " USING ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, &additional_conds, &context);
+ pfree(additional_conds.data);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 144c114d0fe..14579030fab 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2001,23 +2001,16 @@ SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER B
Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END, r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
(4 rows)
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Limit
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1
- -> Merge Semi Join
- Output: t1.c1
- Merge Cond: (t1.c1 = t2.c1)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
- -> Foreign Scan on public.ft2 t2
- Output: t2.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
-(11 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1" FROM "S 1"."T 1" r1 WHERE (EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r2."C 1" = r1."C 1")))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
+(4 rows)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
c1
@@ -4055,23 +4048,13 @@ EXECUTE st2(101, 121);
-- subquery using immutable function (can be sent to remote)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c5) = '1970-01-17'::date) ORDER BY c1;
EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st3(10, 20);
- QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Sort Key: t1.c1
- -> Nested Loop Semi Join
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Join Filter: (t2.c3 = t1.c3)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 20))
- -> Materialize
- Output: t2.c3
- -> Foreign Scan on public.ft2 t2
- Output: t2.c3
- Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE (("C 1" > 10)) AND ((date(c5) = '1970-01-17'::date))
-(14 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" < 20)) AND (EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r3."C 1" > 10)) AND ((date(r3.c5) = '1970-01-17'::date)) AND ((r3.c3 = r1.c3)))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
@@ -4615,6 +4598,283 @@ explain (verbose, costs off) select * from ft3 f, loct3 l
Index Cond: (l.f1 = 'foo'::text)
(12 rows)
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r2.c1, r2.c2, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 900)))) WHERE (EXISTS (SELECT NULL FROM "S 1"."T 4" r4 WHERE ((r1.c2 = r4.c1)))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r4.c1, r4.c2, r4.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r4 ON (((r1.c2 = r4.c1)) AND ((r1."C 1" > 900)))) WHERE (EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1)))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: (public.ft2) LEFT JOIN ((public.ft4) SEMI JOIN (public.ft5))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, s6.c1, s6.c2, s6.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r4.c1, r4.c2, r4.c3 FROM "S 1"."T 3" r4 WHERE (EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1))))) s6(c1, c2, c3) ON (((r1.c2 = s6.c1)))) WHERE ((r1."C 1" > 900)) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 901 | 1 | 00901 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo | | |
+ 902 | 2 | 00902 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2 | foo | | |
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo | | |
+ 904 | 4 | 00904 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4 | foo | | |
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo | | |
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo | | |
+ 908 | 8 | 00908 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8 | foo | | |
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo | | |
+ 910 | 0 | 00910 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0 | foo | | |
+(10 rows)
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)) INNER JOIN (public.ft5 ft5_1)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 4" r8 ON (((r1.c2 <= r8.c1)))) WHERE (EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r8.c1 = r9.c1))) AND EXISTS (SELECT NULL FROM "S 1"."T 4" r7 WHERE ((r6.c1 = r7.c1)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+ Relations: (public.ft2) SEMI JOIN ((public.ft2 ft2_1) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND (EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r1."C 1" = r3."C 1")) AND (EXISTS (SELECT NULL FROM "S 1"."T 3" r4 WHERE ((r3.c2 = r4.c2)))))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ftupper.c1, ftupper.c2, ftupper.c3, ftupper.c4, ftupper.c5, ftupper.c6, ftupper.c7, ftupper.c8
+ Relations: (public.ft2 ftupper) SEMI JOIN ((public.ft2) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND (EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r1."C 1" = r2."C 1")) AND (EXISTS (SELECT NULL FROM "S 1"."T 3" r3 WHERE ((r2.c2 = r3.c2)))))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft2 ft2_1)) INNER JOIN (public.ft2 ft2_2)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 1" r8 ON (((r1.c2 = r8.c2)))) WHERE (EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r1.c2 = r9.c2))) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r7 WHERE ((r7.c2 = r6.c2)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+----+----+----+----+----+----+----+----+----+----+----
+(0 rows)
+
+-- Can't push down semi-join with inner rel vars in targetlist
+EXPLAIN (verbose, costs off)
+SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
+ ft1.c1 IN (
+ SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1)
+ ORDER BY ft1.c1 LIMIT 5;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: ft1.c1
+ -> Merge Semi Join
+ Output: ft1.c1
+ Merge Cond: (ft1.c1 = ft2_1.c1)
+ -> Foreign Scan
+ Output: ft1.c1, ft2.c1
+ Relations: (public.ft1) INNER JOIN (public.ft2)
+ Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1."C 1" ASC NULLS LAST
+ -> Foreign Scan
+ Output: ft2_1.c1, ft4.c1
+ Relations: (public.ft2 ft2_1) INNER JOIN (public.ft4)
+ Remote SQL: SELECT r5."C 1", r6.c1 FROM ("S 1"."T 1" r5 INNER JOIN "S 1"."T 3" r6 ON (((r5."C 1" = r6.c1)))) ORDER BY r5."C 1" ASC NULLS LAST
+(13 rows)
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 8b3206ceaa3..f718798ff7c 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -779,6 +779,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
fpinfo->make_outerrel_subquery = false;
fpinfo->make_innerrel_subquery = false;
fpinfo->lower_subquery_rels = NULL;
+ fpinfo->hidden_subquery_rels = NULL;
/* Set the relation index. */
fpinfo->relation_index = baserel->relid;
}
@@ -5724,6 +5725,45 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
return commands;
}
+/*
+ * Check if reltarget is safe enough to push down semi-join.
+ * Reltarget is not safe, if it contains references
+ * to inner rel relids, which do not belong to outer rel.
+ */
+static bool
+semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, RelOptInfo *innerrel)
+{
+ List *vars;
+ ListCell *lc;
+ bool ok = true;
+
+ Assert(joinrel->reltarget);
+
+ vars = pull_var_clause((Node *) joinrel->reltarget->exprs, PVC_INCLUDE_PLACEHOLDERS);
+
+ foreach(lc, vars)
+ {
+ Var *var = (Var *) lfirst(lc);
+
+ if (!IsA(var, Var))
+ continue;
+
+ if (bms_is_member(var->varno, innerrel->relids) &&
+ !bms_is_member(var->varno, outerrel->relids))
+ {
+ /*
+ * Planner can create semi-joins, which refer to inner rel vars in
+ * its target list. However, we deparse semi-join as exists()
+ * subquery, so can't handle references to inner rel in target
+ * list.
+ */
+ ok = false;
+ break;
+ }
+ }
+ return ok;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -5741,12 +5781,19 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
List *joinclauses;
/*
- * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
- * Constructing queries representing SEMI and ANTI joins is hard, hence
- * not considered right now.
+ * We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
+ * Constructing queries representing ANTI joins is hard, hence not
+ * considered right now.
*/
if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
- jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+ jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+ jointype != JOIN_SEMI)
+ return false;
+
+ /*
+ * We can't push down semi-join if its reltarget is not safe
+ */
+ if ((jointype == JOIN_SEMI) && !semijoin_target_ok(root, joinrel, outerrel, innerrel))
return false;
/*
@@ -5858,6 +5905,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
fpinfo->lower_subquery_rels = bms_union(fpinfo_o->lower_subquery_rels,
fpinfo_i->lower_subquery_rels);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo_o->hidden_subquery_rels,
+ fpinfo_i->hidden_subquery_rels);
/*
* Pull the other remote conditions from the joining relations into join
@@ -5871,6 +5920,12 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
* the joinclauses, since they need to be evaluated while constructing the
* join.
*
+ * For SEMI-JOIN clauses from inner relation can not be added to remote_conds,
+ * but should be treated as join clauses (as they are deparsed to EXISTS
+ * subquery, where inner relation can be referred). A list of relation ids,
+ * which can't be referred from higher levels, is preserved as
+ * hidden_subquery_rels list.
+ *
* For a FULL OUTER JOIN, the other clauses from either relation can not
* be added to the joinclauses or remote_conds, since each relation acts
* as an outer relation for the other.
@@ -5901,6 +5956,16 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo_i->remote_conds);
break;
+ case JOIN_SEMI:
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo_i->remote_conds);
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo->remote_conds);
+ fpinfo->remote_conds = list_copy(fpinfo_o->remote_conds);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo->hidden_subquery_rels,
+ innerrel->relids);
+ break;
+
case JOIN_FULL:
/*
@@ -5943,6 +6008,24 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo->joinclauses = fpinfo->remote_conds;
fpinfo->remote_conds = NIL;
}
+ else if (jointype == JOIN_LEFT || jointype == JOIN_RIGHT || jointype == JOIN_FULL)
+ {
+ /*
+ * Conditions, generated from semi-joins, should be evaluated before
+ * LEFT/RIGHT/FULL join.
+ */
+ if (!bms_is_empty(fpinfo_o->hidden_subquery_rels))
+ {
+ fpinfo->make_outerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, outerrel->relids);
+ }
+
+ if (!bms_is_empty(fpinfo_i->hidden_subquery_rels))
+ {
+ fpinfo->make_innerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, innerrel->relids);
+ }
+ }
/* Mark that this join can be pushed down safely */
fpinfo->pushdown_safe = true;
@@ -7692,6 +7775,8 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
{
ListCell *lc;
+ PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
+
foreach(lc, ec->ec_members)
{
EquivalenceMember *em = (EquivalenceMember *) lfirst(lc);
@@ -7702,6 +7787,7 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
*/
if (bms_is_subset(em->em_relids, rel->relids) &&
!bms_is_empty(em->em_relids) &&
+ bms_is_empty(bms_intersect(em->em_relids, fpinfo->hidden_subquery_rels)) &&
is_foreign_expr(root, rel, em->em_expr))
return em;
}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 47157ac887e..1881292ce98 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -118,6 +118,9 @@ typedef struct PgFdwRelationInfo
* subquery? */
Relids lower_subquery_rels; /* all relids appearing in lower
* subqueries */
+ Relids hidden_subquery_rels; /* relids, which can't be referred to
+ * from upper relations,
+ * used internally for em search */
/*
* Index of the relation. It is used to create an alias to a subquery
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index a303bfb322d..76ba154cbc3 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -600,7 +600,7 @@ WITH t (c1_1, c1_3, c2_1) AS MATERIALIZED (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t
-- ctid with whole-row reference
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
@@ -1301,6 +1301,130 @@ explain (verbose, costs off) select * from ft3 where f2 = 'foo' COLLATE "C";
explain (verbose, costs off) select * from ft3 f, loct3 l
where f.f3 = l.f3 COLLATE "POSIX" and l.f1 = 'foo';
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Can't push down semi-join with inner rel vars in targetlist
+EXPLAIN (verbose, costs off)
+SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
+ ft1.c1 IN (
+ SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1)
+ ORDER BY ft1.c1 LIMIT 5;
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
--
2.34.1
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2023-11-27 00:49 Alexander Korotkov <[email protected]>
parent: Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Korotkov @ 2023-11-27 00:49 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>
Hi, Alexander!
On Tue, Oct 31, 2023 at 1:07 PM Alexander Pyhalov
<[email protected]> wrote:
> There are several cases when we can't push down semi-join in current
> patch.
>
> 1) When target list has attributes from inner relation, which are
> equivalent to some attributes of outer
> relation, we fail to notice this.
>
> 2) When we examine A join B and decide that we can't push it down, this
> decision is final - we state it in fdw_private of joinrel,
> and so if we consider joining these relations in another order, we don't
> reconsider.
> This means that if later examine B join A, we don't try to push it down.
> As semi-join can be executed as JOIN_UNIQUE_INNER or JOIN_UNIQUE_OUTER,
> this can be a problem - we look at some of these paths and remember that
> we can't push down such join.
Thank you for the revision.
I've revised the patch myself. I've replaced StringInfo with
additional conds into a list of strings as I proposed before. I think
the code became much clearer. Also, it gets rid of some unnecessary
allocations.
I think the code itself is not in bad shape. But patch lacks some
high-level description of semi-joins processing as well as comments on
each manipulation with additional conds. Could you please add this?
------
Regards,
Alexander Korotkov
Attachments:
[application/octet-stream] 0001-postgres_fdw-add-support-for-deparsing-semi-joins-v6.patch (51.0K, ../../CAPpHfdtYndP0+P4YjJrNAgX6X6xVL6z4n0L_u3P=se+Xt0VksA@mail.gmail.com/2-0001-postgres_fdw-add-support-for-deparsing-semi-joins-v6.patch)
download | inline diff:
From 77add3c5532b3505eafd886e28d5101c13f4c81f Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Mon, 7 Nov 2022 10:23:32 +0300
Subject: [PATCH] postgres_fdw: add support for deparsing semi joins
We deparse semi-joins as EXISTS subqueries. So, deparsing
semi-join leads to generating addl_conds condition,
which is then added to the uppermost JOIN's WHERE clause.
---
contrib/postgres_fdw/deparse.c | 205 ++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 320 ++++++++++++++++--
contrib/postgres_fdw/postgres_fdw.c | 94 ++++-
contrib/postgres_fdw/postgres_fdw.h | 3 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 126 ++++++-
5 files changed, 667 insertions(+), 81 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 09fd489a901..408d80d235c 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -180,11 +180,15 @@ static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
Index ignore_rel, List **ignore_conds,
+ List **additional_conds,
List **params_list);
+static void appendWhereClause(List *exprs, List *additional_conds,
+ deparse_expr_cxt *context);
static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool make_subquery,
- Index ignore_rel, List **ignore_conds, List **params_list);
+ Index ignore_rel, List **ignore_conds,
+ List **additional_conds, List **params_list);
static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
static void appendOrderBySuffix(Oid sortop, Oid sortcoltype, bool nulls_first,
@@ -1370,6 +1374,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
{
StringInfo buf = context->buf;
RelOptInfo *scanrel = context->scanrel;
+ List *additional_conds = NIL;
/* For upper relations, scanrel must be either a joinrel or a baserel */
Assert(!IS_UPPER_REL(context->foreignrel) ||
@@ -1379,14 +1384,11 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, context->root, scanrel,
(bms_membership(scanrel->relids) == BMS_MULTIPLE),
- (Index) 0, NULL, context->params_list);
-
- /* Construct WHERE clause */
- if (quals != NIL)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(quals, context);
- }
+ (Index) 0, NULL, &additional_conds,
+ context->params_list);
+ appendWhereClause(quals, additional_conds, context);
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
}
/*
@@ -1598,6 +1600,35 @@ appendConditions(List *exprs, deparse_expr_cxt *context)
reset_transmission_modes(nestlevel);
}
+/*
+ * Append WHERE clause, containing conditions
+ * from exprs and additional_conds, to context->buf.
+ */
+static void
+appendWhereClause(List *exprs, List *additional_conds, deparse_expr_cxt *context)
+{
+ StringInfo buf = context->buf;
+ bool need_and = false;
+ ListCell *lc;
+
+ if (exprs != NIL || additional_conds != NIL)
+ appendStringInfoString(buf, " WHERE ");
+
+ if (exprs != NIL)
+ {
+ appendConditions(exprs, context);
+ need_and = true;
+ }
+
+ foreach(lc, additional_conds)
+ {
+ if (need_and)
+ appendStringInfoString(buf, " AND ");
+ appendStringInfoString(buf, (char *) lc->ptr_value);
+ need_and = true;
+ }
+}
+
/* Output join name for given join type */
const char *
get_jointype_name(JoinType jointype)
@@ -1616,6 +1647,9 @@ get_jointype_name(JoinType jointype)
case JOIN_FULL:
return "FULL";
+ case JOIN_SEMI:
+ return "SEMI";
+
default:
/* Shouldn't come here, but protect from buggy code. */
elog(ERROR, "unsupported join type %d", jointype);
@@ -1712,11 +1746,14 @@ deparseSubqueryTargetList(deparse_expr_cxt *context)
* of DELETE; it deparses the join relation as if the relation never contained
* the target relation, and creates a List of conditions to be deparsed into
* the top-level WHERE clause, which is returned to *ignore_conds.
+ *
+ * 'additional_conds' is a pointer to list of strings to be appended to WHERE
+ * clause.
*/
static void
deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool use_alias, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ List **additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1728,6 +1765,8 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
RelOptInfo *innerrel = fpinfo->innerrel;
bool outerrel_is_target = false;
bool innerrel_is_target = false;
+ List *additional_conds_i = NIL;
+ List *additional_conds_o = NIL;
if (ignore_rel > 0 && bms_is_member(ignore_rel, foreignrel->relids))
{
@@ -1764,7 +1803,8 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
initStringInfo(&join_sql_o);
deparseRangeTblRef(&join_sql_o, root, outerrel,
fpinfo->make_outerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_o,
+ params_list);
/*
* If inner relation is the target relation, skip deparsing it.
@@ -1780,6 +1820,11 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_o.data, join_sql_o.len);
+ if (additional_conds_o != NIL)
+ {
+ Assert(*additional_conds == NIL);
+ *additional_conds = additional_conds_o;
+ }
return;
}
}
@@ -1790,7 +1835,37 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
initStringInfo(&join_sql_i);
deparseRangeTblRef(&join_sql_i, root, innerrel,
fpinfo->make_innerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_i,
+ params_list);
+
+ if (fpinfo->jointype == JOIN_SEMI)
+ {
+ deparse_expr_cxt context;
+ StringInfoData str;
+
+ initStringInfo(&str);
+ appendStringInfo(&str, "EXISTS (SELECT NULL FROM %s",
+ join_sql_i.data);
+
+ context.buf = &str;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
+
+ appendWhereClause(fpinfo->joinclauses, additional_conds_i, &context);
+
+ if (additional_conds_i != NIL)
+ {
+ list_free_deep(additional_conds_i);
+ additional_conds_i = NIL;
+ }
+
+ /* Close parentheses for EXISTS subquery */
+ appendStringInfo(&str, ")");
+
+ *additional_conds = lappend(*additional_conds, str.data);
+ }
/*
* If outer relation is the target relation, skip deparsing it.
@@ -1801,6 +1876,11 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_i.data, join_sql_i.len);
+ if (additional_conds_i != NIL)
+ {
+ Assert(*additional_conds == NIL);
+ *additional_conds = additional_conds_i;
+ }
return;
}
}
@@ -1809,33 +1889,64 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(!outerrel_is_target && !innerrel_is_target);
/*
- * For a join relation FROM clause entry is deparsed as
- *
- * ((outer relation) <join type> (inner relation) ON (joinclauses))
+ * For semijoin FROM clause is deparsed as an outer relation. An inner
+ * relation and join clauses are converted to EXISTS condition and
+ * passed to the upper level.
*/
- appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
- get_jointype_name(fpinfo->jointype), join_sql_i.data);
-
- /* Append join clause; (TRUE) if no join clause */
- if (fpinfo->joinclauses)
+ if (fpinfo->jointype == JOIN_SEMI)
{
- deparse_expr_cxt context;
+ appendStringInfo(buf, "%s", join_sql_o.data);
+ }
+ else
+ {
+ /*
+ * For a join relation FROM clause entry is deparsed as
+ *
+ * ((outer relation) <join type> (inner relation) ON
+ * (joinclauses))
+ */
+ appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
+ get_jointype_name(fpinfo->jointype), join_sql_i.data);
+
+ /* Append join clause; (TRUE) if no join clause */
+ if (fpinfo->joinclauses)
+ {
+ deparse_expr_cxt context;
- context.buf = buf;
- context.foreignrel = foreignrel;
- context.scanrel = foreignrel;
- context.root = root;
- context.params_list = params_list;
+ context.buf = buf;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
- appendStringInfoChar(buf, '(');
- appendConditions(fpinfo->joinclauses, &context);
+ appendStringInfoChar(buf, '(');
+ appendConditions(fpinfo->joinclauses, &context);
+ appendStringInfoChar(buf, ')');
+ }
+ else
+ appendStringInfoString(buf, "(TRUE)");
+
+ /* End the FROM clause entry. */
appendStringInfoChar(buf, ')');
}
- else
- appendStringInfoString(buf, "(TRUE)");
- /* End the FROM clause entry. */
- appendStringInfoChar(buf, ')');
+ /*
+ * Construct additional_conds from current level additional_conds and
+ * additional_conds, coming from inner and outer rels.
+ */
+ if (additional_conds_o != NIL)
+ {
+ *additional_conds = list_concat(*additional_conds,
+ additional_conds_o);
+ list_free(additional_conds_o);
+ }
+
+ if (additional_conds_i != NIL)
+ {
+ *additional_conds = list_concat(*additional_conds,
+ additional_conds_i);
+ list_free(additional_conds_i);
+ }
}
else
{
@@ -1867,7 +1978,7 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
static void
deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ List **additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1925,7 +2036,8 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
else
deparseFromExprForRel(buf, root, foreignrel, true, ignore_rel,
- ignore_conds, params_list);
+ ignore_conds, additional_conds,
+ params_list);
}
/*
@@ -2148,6 +2260,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
ListCell *lc,
*lc2;
+ List *additional_conds = NIL;
/* Set up context struct for recursion */
context.root = root;
@@ -2189,17 +2302,17 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
{
List *ignore_conds = NIL;
+
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, additional_conds, &context);
+
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
@@ -2255,6 +2368,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
List **retrieved_attrs)
{
deparse_expr_cxt context;
+ List *additional_conds = NIL;
/* Set up context struct for recursion */
context.root = root;
@@ -2274,15 +2388,14 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
appendStringInfoString(buf, " USING ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, additional_conds, &context);
+
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 22cae37a1eb..36e8939076a 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2001,23 +2001,16 @@ SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER B
Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END, r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
(4 rows)
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Limit
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1
- -> Merge Semi Join
- Output: t1.c1
- Merge Cond: (t1.c1 = t2.c1)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
- -> Foreign Scan on public.ft2 t2
- Output: t2.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
-(11 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1" FROM "S 1"."T 1" r1 WHERE EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r2."C 1" = r1."C 1"))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
+(4 rows)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
c1
@@ -4056,23 +4049,13 @@ RESET enable_sort;
-- subquery using immutable function (can be sent to remote)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c5) = '1970-01-17'::date) ORDER BY c1;
EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st3(10, 20);
- QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Sort Key: t1.c1
- -> Nested Loop Semi Join
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Join Filter: (t2.c3 = t1.c3)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 20))
- -> Materialize
- Output: t2.c3
- -> Foreign Scan on public.ft2 t2
- Output: t2.c3
- Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE (("C 1" > 10)) AND ((date(c5) = '1970-01-17'::date))
-(14 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" < 20)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r3."C 1" > 10)) AND ((date(r3.c5) = '1970-01-17'::date)) AND ((r3.c3 = r1.c3))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
@@ -4616,6 +4599,283 @@ explain (verbose, costs off) select * from ft3 f, loct3 l
Index Cond: (l.f1 = 'foo'::text)
(12 rows)
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r2.c1, r2.c2, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 900)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r4 WHERE ((r1.c2 = r4.c1))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r4.c1, r4.c2, r4.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r4 ON (((r1.c2 = r4.c1)) AND ((r1."C 1" > 900)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: (public.ft2) LEFT JOIN ((public.ft4) SEMI JOIN (public.ft5))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, s6.c1, s6.c2, s6.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r4.c1, r4.c2, r4.c3 FROM "S 1"."T 3" r4 WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1)))) s6(c1, c2, c3) ON (((r1.c2 = s6.c1)))) WHERE ((r1."C 1" > 900)) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 901 | 1 | 00901 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo | | |
+ 902 | 2 | 00902 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2 | foo | | |
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo | | |
+ 904 | 4 | 00904 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4 | foo | | |
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo | | |
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo | | |
+ 908 | 8 | 00908 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8 | foo | | |
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo | | |
+ 910 | 0 | 00910 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0 | foo | | |
+(10 rows)
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)) INNER JOIN (public.ft5 ft5_1)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 4" r8 ON (((r1.c2 <= r8.c1)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r8.c1 = r9.c1))) AND EXISTS (SELECT NULL FROM "S 1"."T 4" r7 WHERE ((r6.c1 = r7.c1))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+ Relations: (public.ft2) SEMI JOIN ((public.ft2 ft2_1) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r1."C 1" = r3."C 1")) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r4 WHERE ((r3.c2 = r4.c2)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ftupper.c1, ftupper.c2, ftupper.c3, ftupper.c4, ftupper.c5, ftupper.c6, ftupper.c7, ftupper.c8
+ Relations: (public.ft2 ftupper) SEMI JOIN ((public.ft2) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r1."C 1" = r2."C 1")) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r3 WHERE ((r2.c2 = r3.c2)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft2 ft2_1)) INNER JOIN (public.ft2 ft2_2)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 1" r8 ON (((r1.c2 = r8.c2)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r1.c2 = r9.c2))) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r7 WHERE ((r7.c2 = r6.c2))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+----+----+----+----+----+----+----+----+----+----+----
+(0 rows)
+
+-- Can't push down semi-join with inner rel vars in targetlist
+EXPLAIN (verbose, costs off)
+SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
+ ft1.c1 IN (
+ SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1)
+ ORDER BY ft1.c1 LIMIT 5;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: ft1.c1
+ -> Merge Semi Join
+ Output: ft1.c1
+ Merge Cond: (ft1.c1 = ft2_1.c1)
+ -> Foreign Scan
+ Output: ft1.c1, ft2.c1
+ Relations: (public.ft1) INNER JOIN (public.ft2)
+ Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1."C 1" ASC NULLS LAST
+ -> Foreign Scan
+ Output: ft2_1.c1, ft4.c1
+ Relations: (public.ft2 ft2_1) INNER JOIN (public.ft4)
+ Remote SQL: SELECT r5."C 1", r6.c1 FROM ("S 1"."T 1" r5 INNER JOIN "S 1"."T 3" r6 ON (((r5."C 1" = r6.c1)))) ORDER BY r5."C 1" ASC NULLS LAST
+(13 rows)
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6de2bec3b7b..3b0ccbb22ca 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -779,6 +779,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
fpinfo->make_outerrel_subquery = false;
fpinfo->make_innerrel_subquery = false;
fpinfo->lower_subquery_rels = NULL;
+ fpinfo->hidden_subquery_rels = NULL;
/* Set the relation index. */
fpinfo->relation_index = baserel->relid;
}
@@ -5724,6 +5725,45 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
return commands;
}
+/*
+ * Check if reltarget is safe enough to push down semi-join.
+ * Reltarget is not safe, if it contains references
+ * to inner rel relids, which do not belong to outer rel.
+ */
+static bool
+semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, RelOptInfo *innerrel)
+{
+ List *vars;
+ ListCell *lc;
+ bool ok = true;
+
+ Assert(joinrel->reltarget);
+
+ vars = pull_var_clause((Node *) joinrel->reltarget->exprs, PVC_INCLUDE_PLACEHOLDERS);
+
+ foreach(lc, vars)
+ {
+ Var *var = (Var *) lfirst(lc);
+
+ if (!IsA(var, Var))
+ continue;
+
+ if (bms_is_member(var->varno, innerrel->relids) &&
+ !bms_is_member(var->varno, outerrel->relids))
+ {
+ /*
+ * Planner can create semi-joins, which refer to inner rel vars in
+ * its target list. However, we deparse semi-join as exists()
+ * subquery, so can't handle references to inner rel in target
+ * list.
+ */
+ ok = false;
+ break;
+ }
+ }
+ return ok;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -5741,12 +5781,19 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
List *joinclauses;
/*
- * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
- * Constructing queries representing SEMI and ANTI joins is hard, hence
- * not considered right now.
+ * We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
+ * Constructing queries representing ANTI joins is hard, hence not
+ * considered right now.
*/
if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
- jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+ jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+ jointype != JOIN_SEMI)
+ return false;
+
+ /*
+ * We can't push down semi-join if its reltarget is not safe
+ */
+ if ((jointype == JOIN_SEMI) && !semijoin_target_ok(root, joinrel, outerrel, innerrel))
return false;
/*
@@ -5858,6 +5905,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
fpinfo->lower_subquery_rels = bms_union(fpinfo_o->lower_subquery_rels,
fpinfo_i->lower_subquery_rels);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo_o->hidden_subquery_rels,
+ fpinfo_i->hidden_subquery_rels);
/*
* Pull the other remote conditions from the joining relations into join
@@ -5871,6 +5920,12 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
* the joinclauses, since they need to be evaluated while constructing the
* join.
*
+ * For SEMI-JOIN clauses from inner relation can not be added to
+ * remote_conds, but should be treated as join clauses (as they are
+ * deparsed to EXISTS subquery, where inner relation can be referred). A
+ * list of relation ids, which can't be referred from higher levels, is
+ * preserved as hidden_subquery_rels list.
+ *
* For a FULL OUTER JOIN, the other clauses from either relation can not
* be added to the joinclauses or remote_conds, since each relation acts
* as an outer relation for the other.
@@ -5901,6 +5956,16 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo_i->remote_conds);
break;
+ case JOIN_SEMI:
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo_i->remote_conds);
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo->remote_conds);
+ fpinfo->remote_conds = list_copy(fpinfo_o->remote_conds);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo->hidden_subquery_rels,
+ innerrel->relids);
+ break;
+
case JOIN_FULL:
/*
@@ -5943,6 +6008,24 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo->joinclauses = fpinfo->remote_conds;
fpinfo->remote_conds = NIL;
}
+ else if (jointype == JOIN_LEFT || jointype == JOIN_RIGHT || jointype == JOIN_FULL)
+ {
+ /*
+ * Conditions, generated from semi-joins, should be evaluated before
+ * LEFT/RIGHT/FULL join.
+ */
+ if (!bms_is_empty(fpinfo_o->hidden_subquery_rels))
+ {
+ fpinfo->make_outerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, outerrel->relids);
+ }
+
+ if (!bms_is_empty(fpinfo_i->hidden_subquery_rels))
+ {
+ fpinfo->make_innerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, innerrel->relids);
+ }
+ }
/* Mark that this join can be pushed down safely */
fpinfo->pushdown_safe = true;
@@ -7692,6 +7775,8 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
{
ListCell *lc;
+ PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
+
foreach(lc, ec->ec_members)
{
EquivalenceMember *em = (EquivalenceMember *) lfirst(lc);
@@ -7702,6 +7787,7 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
*/
if (bms_is_subset(em->em_relids, rel->relids) &&
!bms_is_empty(em->em_relids) &&
+ bms_is_empty(bms_intersect(em->em_relids, fpinfo->hidden_subquery_rels)) &&
is_foreign_expr(root, rel, em->em_expr))
return em;
}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 47157ac887e..d6e4d9e78de 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -118,6 +118,9 @@ typedef struct PgFdwRelationInfo
* subquery? */
Relids lower_subquery_rels; /* all relids appearing in lower
* subqueries */
+ Relids hidden_subquery_rels; /* relids, which can't be referred to
+ * from upper relations, used
+ * internally for em search */
/*
* Index of the relation. It is used to create an alias to a subquery
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 075da4ff867..cb405407028 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -600,7 +600,7 @@ WITH t (c1_1, c1_3, c2_1) AS MATERIALIZED (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t
-- ctid with whole-row reference
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
@@ -1305,6 +1305,130 @@ explain (verbose, costs off) select * from ft3 where f2 = 'foo' COLLATE "C";
explain (verbose, costs off) select * from ft3 f, loct3 l
where f.f3 = l.f3 COLLATE "POSIX" and l.f1 = 'foo';
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Can't push down semi-join with inner rel vars in targetlist
+EXPLAIN (verbose, costs off)
+SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
+ ft1.c1 IN (
+ SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1)
+ ORDER BY ft1.c1 LIMIT 5;
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
--
2.39.3 (Apple Git-145)
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2023-11-27 15:11 Alexander Pyhalov <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Pyhalov @ 2023-11-27 15:11 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>
Alexander Korotkov писал(а) 2023-11-27 03:49:
> Thank you for the revision.
>
> I've revised the patch myself. I've replaced StringInfo with
> additional conds into a list of strings as I proposed before. I think
> the code became much clearer. Also, it gets rid of some unnecessary
> allocations.
>
> I think the code itself is not in bad shape. But patch lacks some
> high-level description of semi-joins processing as well as comments on
> each manipulation with additional conds. Could you please add this?
>
Hi. The updated patch looks better. It seems I've failed to fix logic in
deparseFromExprForRel() when tried to convert StringInfos to Lists.
I've added some comments. The most complete description of how SEMI-JOIN
is processed, is located in deparseFromExprForRel(). Unfortunately,
there seems to be no single place, describing current JOIN deparsing
logic.
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] v7-0001-postgres_fdw-add-support-for-deparsing-semi-joins.patch (52.1K, ../../[email protected]/2-v7-0001-postgres_fdw-add-support-for-deparsing-semi-joins.patch)
download | inline diff:
From c17e05d09d5721d22785ed11bed053162d67d967 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Mon, 27 Nov 2023 14:35:29 +0300
Subject: [PATCH] postgres_fdw: add support for deparsing semi joins
---
contrib/postgres_fdw/deparse.c | 234 ++++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 320 ++++++++++++++++--
contrib/postgres_fdw/postgres_fdw.c | 94 ++++-
contrib/postgres_fdw/postgres_fdw.h | 3 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 126 ++++++-
5 files changed, 696 insertions(+), 81 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 09fd489a901..8670524578b 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -180,11 +180,15 @@ static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
Index ignore_rel, List **ignore_conds,
+ List **additional_conds,
List **params_list);
+static void appendWhereClause(List *exprs, List *additional_conds,
+ deparse_expr_cxt *context);
static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool make_subquery,
- Index ignore_rel, List **ignore_conds, List **params_list);
+ Index ignore_rel, List **ignore_conds,
+ List **additional_conds, List **params_list);
static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
static void appendOrderBySuffix(Oid sortop, Oid sortcoltype, bool nulls_first,
@@ -1370,6 +1374,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
{
StringInfo buf = context->buf;
RelOptInfo *scanrel = context->scanrel;
+ List *additional_conds = NIL;
/* For upper relations, scanrel must be either a joinrel or a baserel */
Assert(!IS_UPPER_REL(context->foreignrel) ||
@@ -1379,14 +1384,11 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, context->root, scanrel,
(bms_membership(scanrel->relids) == BMS_MULTIPLE),
- (Index) 0, NULL, context->params_list);
-
- /* Construct WHERE clause */
- if (quals != NIL)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(quals, context);
- }
+ (Index) 0, NULL, &additional_conds,
+ context->params_list);
+ appendWhereClause(quals, additional_conds, context);
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
}
/*
@@ -1598,6 +1600,42 @@ appendConditions(List *exprs, deparse_expr_cxt *context)
reset_transmission_modes(nestlevel);
}
+/*
+ * Append WHERE clause, containing conditions
+ * from exprs and additional_conds, to context->buf.
+ */
+static void
+appendWhereClause(List *exprs, List *additional_conds, deparse_expr_cxt *context)
+{
+ StringInfo buf = context->buf;
+ bool need_and = false;
+ ListCell *lc;
+
+ if (exprs != NIL || additional_conds != NIL)
+ appendStringInfoString(buf, " WHERE ");
+
+ /*
+ * If there are some filters, append them.
+ */
+ if (exprs != NIL)
+ {
+ appendConditions(exprs, context);
+ need_and = true;
+ }
+
+ /*
+ * If there are some EXISTS conditions, coming from SEMI-JOINS, append
+ * them.
+ */
+ foreach(lc, additional_conds)
+ {
+ if (need_and)
+ appendStringInfoString(buf, " AND ");
+ appendStringInfoString(buf, (char *) lfirst(lc));
+ need_and = true;
+ }
+}
+
/* Output join name for given join type */
const char *
get_jointype_name(JoinType jointype)
@@ -1616,6 +1654,9 @@ get_jointype_name(JoinType jointype)
case JOIN_FULL:
return "FULL";
+ case JOIN_SEMI:
+ return "SEMI";
+
default:
/* Shouldn't come here, but protect from buggy code. */
elog(ERROR, "unsupported join type %d", jointype);
@@ -1712,11 +1753,14 @@ deparseSubqueryTargetList(deparse_expr_cxt *context)
* of DELETE; it deparses the join relation as if the relation never contained
* the target relation, and creates a List of conditions to be deparsed into
* the top-level WHERE clause, which is returned to *ignore_conds.
+ *
+ * 'additional_conds' is a pointer to list of strings to be appended to WHERE
+ * clause, coming from lower-level SEMI-JOINs.
*/
static void
deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool use_alias, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ List **additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1728,6 +1772,8 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
RelOptInfo *innerrel = fpinfo->innerrel;
bool outerrel_is_target = false;
bool innerrel_is_target = false;
+ List *additional_conds_i = NIL;
+ List *additional_conds_o = NIL;
if (ignore_rel > 0 && bms_is_member(ignore_rel, foreignrel->relids))
{
@@ -1764,7 +1810,8 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
initStringInfo(&join_sql_o);
deparseRangeTblRef(&join_sql_o, root, outerrel,
fpinfo->make_outerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_o,
+ params_list);
/*
* If inner relation is the target relation, skip deparsing it.
@@ -1780,6 +1827,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_o.data, join_sql_o.len);
+ /* Pass EXISTS conditions to upper level */
+ if (additional_conds_o != NIL)
+ {
+ Assert(*additional_conds == NIL);
+ *additional_conds = additional_conds_o;
+ }
return;
}
}
@@ -1790,7 +1843,54 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
initStringInfo(&join_sql_i);
deparseRangeTblRef(&join_sql_i, root, innerrel,
fpinfo->make_innerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_i,
+ params_list);
+
+ /*
+ * SEMI-JOIN is deparsed as EXISTS subquery. It references outer
+ * and inner relations, so should be evaluated as condition in the
+ * upper-level WHERE clause. We deparse the condition and pass it
+ * to upper level callers as additional_conds list. Upper level
+ * callers are responsible to insert conditions from the list
+ * where appropriate.
+ */
+ if (fpinfo->jointype == JOIN_SEMI)
+ {
+ deparse_expr_cxt context;
+ StringInfoData str;
+
+ /* Construct deparsed condition from this SEMI-JOIN */
+ initStringInfo(&str);
+ appendStringInfo(&str, "EXISTS (SELECT NULL FROM %s",
+ join_sql_i.data);
+
+ context.buf = &str;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
+
+ /*
+ * Append SEMI-JOIN clauses and EXISTS conditions from lower
+ * levels to the current EXISTS subquery
+ */
+ appendWhereClause(fpinfo->joinclauses, additional_conds_i, &context);
+
+ /*
+ * EXISTS conditions, coming from lower join levels, has just
+ * been processed.
+ */
+ if (additional_conds_i != NIL)
+ {
+ list_free_deep(additional_conds_i);
+ additional_conds_i = NIL;
+ }
+
+ /* Close parentheses for EXISTS subquery */
+ appendStringInfo(&str, ")");
+
+ *additional_conds = lappend(*additional_conds, str.data);
+ }
/*
* If outer relation is the target relation, skip deparsing it.
@@ -1801,6 +1901,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_i.data, join_sql_i.len);
+ /* Pass EXISTS conditions to the upper call */
+ if (additional_conds_i != NIL)
+ {
+ Assert(*additional_conds == NIL);
+ *additional_conds = additional_conds_i;
+ }
return;
}
}
@@ -1809,33 +1915,65 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(!outerrel_is_target && !innerrel_is_target);
/*
- * For a join relation FROM clause entry is deparsed as
- *
- * ((outer relation) <join type> (inner relation) ON (joinclauses))
+ * For semijoin FROM clause is deparsed as an outer relation. An inner
+ * relation and join clauses are converted to EXISTS condition and
+ * passed to the upper level.
*/
- appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
- get_jointype_name(fpinfo->jointype), join_sql_i.data);
-
- /* Append join clause; (TRUE) if no join clause */
- if (fpinfo->joinclauses)
+ if (fpinfo->jointype == JOIN_SEMI)
{
- deparse_expr_cxt context;
+ appendStringInfo(buf, "%s", join_sql_o.data);
+ }
+ else
+ {
+ /*
+ * For a join relation FROM clause entry is deparsed as
+ *
+ * ((outer relation) <join type> (inner relation) ON
+ * (joinclauses))
+ */
+ appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
+ get_jointype_name(fpinfo->jointype), join_sql_i.data);
- context.buf = buf;
- context.foreignrel = foreignrel;
- context.scanrel = foreignrel;
- context.root = root;
- context.params_list = params_list;
+ /* Append join clause; (TRUE) if no join clause */
+ if (fpinfo->joinclauses)
+ {
+ deparse_expr_cxt context;
- appendStringInfoChar(buf, '(');
- appendConditions(fpinfo->joinclauses, &context);
+ context.buf = buf;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
+
+ appendStringInfoChar(buf, '(');
+ appendConditions(fpinfo->joinclauses, &context);
+ appendStringInfoChar(buf, ')');
+ }
+ else
+ appendStringInfoString(buf, "(TRUE)");
+
+ /* End the FROM clause entry. */
appendStringInfoChar(buf, ')');
}
- else
- appendStringInfoString(buf, "(TRUE)");
- /* End the FROM clause entry. */
- appendStringInfoChar(buf, ')');
+ /*
+ * Construct additional_conds to be passed to the upper caller from
+ * current level additional_conds and additional_conds, coming from
+ * inner and outer rels.
+ */
+ if (additional_conds_o != NIL)
+ {
+ *additional_conds = list_concat(*additional_conds,
+ additional_conds_o);
+ list_free(additional_conds_o);
+ }
+
+ if (additional_conds_i != NIL)
+ {
+ *additional_conds = list_concat(*additional_conds,
+ additional_conds_i);
+ list_free(additional_conds_i);
+ }
}
else
{
@@ -1863,11 +2001,13 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/*
* Append FROM clause entry for the given relation into buf.
+ * Conditions from lower-level SEMI-JOINs are appended to additional_conds
+ * and should be added to upper level WHERE clause.
*/
static void
deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ List **additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1925,7 +2065,8 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
else
deparseFromExprForRel(buf, root, foreignrel, true, ignore_rel,
- ignore_conds, params_list);
+ ignore_conds, additional_conds,
+ params_list);
}
/*
@@ -2148,6 +2289,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
ListCell *lc,
*lc2;
+ List *additional_conds = NIL;
/* Set up context struct for recursion */
context.root = root;
@@ -2189,17 +2331,17 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
{
List *ignore_conds = NIL;
+
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, additional_conds, &context);
+
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
@@ -2255,6 +2397,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
List **retrieved_attrs)
{
deparse_expr_cxt context;
+ List *additional_conds = NIL;
/* Set up context struct for recursion */
context.root = root;
@@ -2274,15 +2417,14 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
appendStringInfoString(buf, " USING ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, additional_conds, &context);
+
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 22cae37a1eb..36e8939076a 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2001,23 +2001,16 @@ SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER B
Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END, r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
(4 rows)
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Limit
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1
- -> Merge Semi Join
- Output: t1.c1
- Merge Cond: (t1.c1 = t2.c1)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
- -> Foreign Scan on public.ft2 t2
- Output: t2.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
-(11 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1" FROM "S 1"."T 1" r1 WHERE EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r2."C 1" = r1."C 1"))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
+(4 rows)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
c1
@@ -4056,23 +4049,13 @@ RESET enable_sort;
-- subquery using immutable function (can be sent to remote)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c5) = '1970-01-17'::date) ORDER BY c1;
EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st3(10, 20);
- QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Sort Key: t1.c1
- -> Nested Loop Semi Join
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Join Filter: (t2.c3 = t1.c3)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 20))
- -> Materialize
- Output: t2.c3
- -> Foreign Scan on public.ft2 t2
- Output: t2.c3
- Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE (("C 1" > 10)) AND ((date(c5) = '1970-01-17'::date))
-(14 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" < 20)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r3."C 1" > 10)) AND ((date(r3.c5) = '1970-01-17'::date)) AND ((r3.c3 = r1.c3))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
@@ -4616,6 +4599,283 @@ explain (verbose, costs off) select * from ft3 f, loct3 l
Index Cond: (l.f1 = 'foo'::text)
(12 rows)
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r2.c1, r2.c2, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 900)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r4 WHERE ((r1.c2 = r4.c1))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r4.c1, r4.c2, r4.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r4 ON (((r1.c2 = r4.c1)) AND ((r1."C 1" > 900)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: (public.ft2) LEFT JOIN ((public.ft4) SEMI JOIN (public.ft5))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, s6.c1, s6.c2, s6.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r4.c1, r4.c2, r4.c3 FROM "S 1"."T 3" r4 WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1)))) s6(c1, c2, c3) ON (((r1.c2 = s6.c1)))) WHERE ((r1."C 1" > 900)) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 901 | 1 | 00901 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo | | |
+ 902 | 2 | 00902 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2 | foo | | |
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo | | |
+ 904 | 4 | 00904 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4 | foo | | |
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo | | |
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo | | |
+ 908 | 8 | 00908 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8 | foo | | |
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo | | |
+ 910 | 0 | 00910 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0 | foo | | |
+(10 rows)
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)) INNER JOIN (public.ft5 ft5_1)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 4" r8 ON (((r1.c2 <= r8.c1)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r8.c1 = r9.c1))) AND EXISTS (SELECT NULL FROM "S 1"."T 4" r7 WHERE ((r6.c1 = r7.c1))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+ Relations: (public.ft2) SEMI JOIN ((public.ft2 ft2_1) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r1."C 1" = r3."C 1")) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r4 WHERE ((r3.c2 = r4.c2)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ftupper.c1, ftupper.c2, ftupper.c3, ftupper.c4, ftupper.c5, ftupper.c6, ftupper.c7, ftupper.c8
+ Relations: (public.ft2 ftupper) SEMI JOIN ((public.ft2) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r1."C 1" = r2."C 1")) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r3 WHERE ((r2.c2 = r3.c2)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft2 ft2_1)) INNER JOIN (public.ft2 ft2_2)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 1" r8 ON (((r1.c2 = r8.c2)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r1.c2 = r9.c2))) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r7 WHERE ((r7.c2 = r6.c2))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+----+----+----+----+----+----+----+----+----+----+----
+(0 rows)
+
+-- Can't push down semi-join with inner rel vars in targetlist
+EXPLAIN (verbose, costs off)
+SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
+ ft1.c1 IN (
+ SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1)
+ ORDER BY ft1.c1 LIMIT 5;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: ft1.c1
+ -> Merge Semi Join
+ Output: ft1.c1
+ Merge Cond: (ft1.c1 = ft2_1.c1)
+ -> Foreign Scan
+ Output: ft1.c1, ft2.c1
+ Relations: (public.ft1) INNER JOIN (public.ft2)
+ Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1."C 1" ASC NULLS LAST
+ -> Foreign Scan
+ Output: ft2_1.c1, ft4.c1
+ Relations: (public.ft2 ft2_1) INNER JOIN (public.ft4)
+ Remote SQL: SELECT r5."C 1", r6.c1 FROM ("S 1"."T 1" r5 INNER JOIN "S 1"."T 3" r6 ON (((r5."C 1" = r6.c1)))) ORDER BY r5."C 1" ASC NULLS LAST
+(13 rows)
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6de2bec3b7b..3b0ccbb22ca 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -779,6 +779,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
fpinfo->make_outerrel_subquery = false;
fpinfo->make_innerrel_subquery = false;
fpinfo->lower_subquery_rels = NULL;
+ fpinfo->hidden_subquery_rels = NULL;
/* Set the relation index. */
fpinfo->relation_index = baserel->relid;
}
@@ -5724,6 +5725,45 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
return commands;
}
+/*
+ * Check if reltarget is safe enough to push down semi-join.
+ * Reltarget is not safe, if it contains references
+ * to inner rel relids, which do not belong to outer rel.
+ */
+static bool
+semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, RelOptInfo *innerrel)
+{
+ List *vars;
+ ListCell *lc;
+ bool ok = true;
+
+ Assert(joinrel->reltarget);
+
+ vars = pull_var_clause((Node *) joinrel->reltarget->exprs, PVC_INCLUDE_PLACEHOLDERS);
+
+ foreach(lc, vars)
+ {
+ Var *var = (Var *) lfirst(lc);
+
+ if (!IsA(var, Var))
+ continue;
+
+ if (bms_is_member(var->varno, innerrel->relids) &&
+ !bms_is_member(var->varno, outerrel->relids))
+ {
+ /*
+ * Planner can create semi-joins, which refer to inner rel vars in
+ * its target list. However, we deparse semi-join as exists()
+ * subquery, so can't handle references to inner rel in target
+ * list.
+ */
+ ok = false;
+ break;
+ }
+ }
+ return ok;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -5741,12 +5781,19 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
List *joinclauses;
/*
- * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
- * Constructing queries representing SEMI and ANTI joins is hard, hence
- * not considered right now.
+ * We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
+ * Constructing queries representing ANTI joins is hard, hence not
+ * considered right now.
*/
if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
- jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+ jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+ jointype != JOIN_SEMI)
+ return false;
+
+ /*
+ * We can't push down semi-join if its reltarget is not safe
+ */
+ if ((jointype == JOIN_SEMI) && !semijoin_target_ok(root, joinrel, outerrel, innerrel))
return false;
/*
@@ -5858,6 +5905,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
fpinfo->lower_subquery_rels = bms_union(fpinfo_o->lower_subquery_rels,
fpinfo_i->lower_subquery_rels);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo_o->hidden_subquery_rels,
+ fpinfo_i->hidden_subquery_rels);
/*
* Pull the other remote conditions from the joining relations into join
@@ -5871,6 +5920,12 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
* the joinclauses, since they need to be evaluated while constructing the
* join.
*
+ * For SEMI-JOIN clauses from inner relation can not be added to
+ * remote_conds, but should be treated as join clauses (as they are
+ * deparsed to EXISTS subquery, where inner relation can be referred). A
+ * list of relation ids, which can't be referred from higher levels, is
+ * preserved as hidden_subquery_rels list.
+ *
* For a FULL OUTER JOIN, the other clauses from either relation can not
* be added to the joinclauses or remote_conds, since each relation acts
* as an outer relation for the other.
@@ -5901,6 +5956,16 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo_i->remote_conds);
break;
+ case JOIN_SEMI:
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo_i->remote_conds);
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo->remote_conds);
+ fpinfo->remote_conds = list_copy(fpinfo_o->remote_conds);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo->hidden_subquery_rels,
+ innerrel->relids);
+ break;
+
case JOIN_FULL:
/*
@@ -5943,6 +6008,24 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo->joinclauses = fpinfo->remote_conds;
fpinfo->remote_conds = NIL;
}
+ else if (jointype == JOIN_LEFT || jointype == JOIN_RIGHT || jointype == JOIN_FULL)
+ {
+ /*
+ * Conditions, generated from semi-joins, should be evaluated before
+ * LEFT/RIGHT/FULL join.
+ */
+ if (!bms_is_empty(fpinfo_o->hidden_subquery_rels))
+ {
+ fpinfo->make_outerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, outerrel->relids);
+ }
+
+ if (!bms_is_empty(fpinfo_i->hidden_subquery_rels))
+ {
+ fpinfo->make_innerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, innerrel->relids);
+ }
+ }
/* Mark that this join can be pushed down safely */
fpinfo->pushdown_safe = true;
@@ -7692,6 +7775,8 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
{
ListCell *lc;
+ PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
+
foreach(lc, ec->ec_members)
{
EquivalenceMember *em = (EquivalenceMember *) lfirst(lc);
@@ -7702,6 +7787,7 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
*/
if (bms_is_subset(em->em_relids, rel->relids) &&
!bms_is_empty(em->em_relids) &&
+ bms_is_empty(bms_intersect(em->em_relids, fpinfo->hidden_subquery_rels)) &&
is_foreign_expr(root, rel, em->em_expr))
return em;
}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 47157ac887e..d6e4d9e78de 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -118,6 +118,9 @@ typedef struct PgFdwRelationInfo
* subquery? */
Relids lower_subquery_rels; /* all relids appearing in lower
* subqueries */
+ Relids hidden_subquery_rels; /* relids, which can't be referred to
+ * from upper relations, used
+ * internally for em search */
/*
* Index of the relation. It is used to create an alias to a subquery
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 075da4ff867..cb405407028 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -600,7 +600,7 @@ WITH t (c1_1, c1_3, c2_1) AS MATERIALIZED (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t
-- ctid with whole-row reference
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
@@ -1305,6 +1305,130 @@ explain (verbose, costs off) select * from ft3 where f2 = 'foo' COLLATE "C";
explain (verbose, costs off) select * from ft3 f, loct3 l
where f.f3 = l.f3 COLLATE "POSIX" and l.f1 = 'foo';
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Can't push down semi-join with inner rel vars in targetlist
+EXPLAIN (verbose, costs off)
+SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
+ ft1.c1 IN (
+ SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1)
+ ORDER BY ft1.c1 LIMIT 5;
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
--
2.34.1
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2023-12-03 20:52 Alexander Korotkov <[email protected]>
parent: Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Korotkov @ 2023-12-03 20:52 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>
Hi, Alexander!
On Mon, Nov 27, 2023 at 5:11 PM Alexander Pyhalov
<[email protected]> wrote:
> Alexander Korotkov писал(а) 2023-11-27 03:49:
>
> > Thank you for the revision.
> >
> > I've revised the patch myself. I've replaced StringInfo with
> > additional conds into a list of strings as I proposed before. I think
> > the code became much clearer. Also, it gets rid of some unnecessary
> > allocations.
> >
> > I think the code itself is not in bad shape. But patch lacks some
> > high-level description of semi-joins processing as well as comments on
> > each manipulation with additional conds. Could you please add this?
> >
>
> Hi. The updated patch looks better. It seems I've failed to fix logic in
> deparseFromExprForRel() when tried to convert StringInfos to Lists.
>
> I've added some comments. The most complete description of how SEMI-JOIN
> is processed, is located in deparseFromExprForRel(). Unfortunately,
> there seems to be no single place, describing current JOIN deparsing
> logic.
Looks good to me. I've made some grammar and formatting adjustments.
Also, I've written the commit message.
Now, I think this looks good. I'm going to push this if no objections.
------
Regards,
Alexander Korotkov
Attachments:
[application/octet-stream] 0001-Add-support-for-deparsing-semi-joins-to-contrib-p-v8.patch (53.1K, ../../CAPpHfdsbqy93Og9_0UrtNmCxE7LRwLWLTrmeHkOfeWtvg5tSAA@mail.gmail.com/2-0001-Add-support-for-deparsing-semi-joins-to-contrib-p-v8.patch)
download | inline diff:
From 1d5e17801d830892827766ea06dbcbd32b4e745c Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Sun, 3 Dec 2023 22:48:26 +0200
Subject: [PATCH] Add support for deparsing semi-joins to contrib/postgres_fdw
SEMI-JOIN is deparsed as the EXISTS subquery. It references outer and inner
relations, so it should be evaluated as the condition in the upper-level WHERE
clause. The signatures of deparseFromExprForRel() and deparseRangeTblRef() are
revised so that they can add conditions to the upper level.
PgFdwRelationInfo now has a hidden_subquery_rels field, referencing the relids
used in the inner parts of semi-join. They can't be referred to from upper
relations and should be used internally for equivalence member searches.
The planner can create semi-joins, which refer to inner rel vars in its target
list. However, we deparse semi-join as an exists() subquery. So we skip the
case when the target list references to inner rel of semi-join.
Author: Alexander Pyhalov
Reviewed-by: Ashutosh Bapat, Ian Lawrence Barwick, Yuuki Fujii, Tomas Vondra
Discussion: https://postgr.es/m/[email protected]
---
contrib/postgres_fdw/deparse.c | 234 ++++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 320 ++++++++++++++++--
contrib/postgres_fdw/postgres_fdw.c | 94 ++++-
contrib/postgres_fdw/postgres_fdw.h | 4 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 126 ++++++-
5 files changed, 697 insertions(+), 81 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 09fd489a901..9de4db3d957 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -180,11 +180,15 @@ static void appendConditions(List *exprs, deparse_expr_cxt *context);
static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool use_alias,
Index ignore_rel, List **ignore_conds,
+ List **additional_conds,
List **params_list);
+static void appendWhereClause(List *exprs, List *additional_conds,
+ deparse_expr_cxt *context);
static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel, bool make_subquery,
- Index ignore_rel, List **ignore_conds, List **params_list);
+ Index ignore_rel, List **ignore_conds,
+ List **additional_conds, List **params_list);
static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
static void appendOrderBySuffix(Oid sortop, Oid sortcoltype, bool nulls_first,
@@ -1370,6 +1374,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
{
StringInfo buf = context->buf;
RelOptInfo *scanrel = context->scanrel;
+ List *additional_conds = NIL;
/* For upper relations, scanrel must be either a joinrel or a baserel */
Assert(!IS_UPPER_REL(context->foreignrel) ||
@@ -1379,14 +1384,11 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, context->root, scanrel,
(bms_membership(scanrel->relids) == BMS_MULTIPLE),
- (Index) 0, NULL, context->params_list);
-
- /* Construct WHERE clause */
- if (quals != NIL)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(quals, context);
- }
+ (Index) 0, NULL, &additional_conds,
+ context->params_list);
+ appendWhereClause(quals, additional_conds, context);
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
}
/*
@@ -1598,6 +1600,42 @@ appendConditions(List *exprs, deparse_expr_cxt *context)
reset_transmission_modes(nestlevel);
}
+/*
+ * Append WHERE clause, containing conditions from exprs and additional_conds,
+ * to context->buf.
+ */
+static void
+appendWhereClause(List *exprs, List *additional_conds, deparse_expr_cxt *context)
+{
+ StringInfo buf = context->buf;
+ bool need_and = false;
+ ListCell *lc;
+
+ if (exprs != NIL || additional_conds != NIL)
+ appendStringInfoString(buf, " WHERE ");
+
+ /*
+ * If there are some filters, append them.
+ */
+ if (exprs != NIL)
+ {
+ appendConditions(exprs, context);
+ need_and = true;
+ }
+
+ /*
+ * If there are some EXISTS conditions, coming from SEMI-JOINS, append
+ * them.
+ */
+ foreach(lc, additional_conds)
+ {
+ if (need_and)
+ appendStringInfoString(buf, " AND ");
+ appendStringInfoString(buf, (char *) lfirst(lc));
+ need_and = true;
+ }
+}
+
/* Output join name for given join type */
const char *
get_jointype_name(JoinType jointype)
@@ -1616,6 +1654,9 @@ get_jointype_name(JoinType jointype)
case JOIN_FULL:
return "FULL";
+ case JOIN_SEMI:
+ return "SEMI";
+
default:
/* Shouldn't come here, but protect from buggy code. */
elog(ERROR, "unsupported join type %d", jointype);
@@ -1712,11 +1753,14 @@ deparseSubqueryTargetList(deparse_expr_cxt *context)
* of DELETE; it deparses the join relation as if the relation never contained
* the target relation, and creates a List of conditions to be deparsed into
* the top-level WHERE clause, which is returned to *ignore_conds.
+ *
+ * 'additional_conds' is a pointer to a list of strings to be appended to
+ * the WHERE clause, coming from lower-level SEMI-JOINs.
*/
static void
deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool use_alias, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ List **additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1728,6 +1772,8 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
RelOptInfo *innerrel = fpinfo->innerrel;
bool outerrel_is_target = false;
bool innerrel_is_target = false;
+ List *additional_conds_i = NIL;
+ List *additional_conds_o = NIL;
if (ignore_rel > 0 && bms_is_member(ignore_rel, foreignrel->relids))
{
@@ -1764,7 +1810,8 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
initStringInfo(&join_sql_o);
deparseRangeTblRef(&join_sql_o, root, outerrel,
fpinfo->make_outerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_o,
+ params_list);
/*
* If inner relation is the target relation, skip deparsing it.
@@ -1780,6 +1827,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_o.data, join_sql_o.len);
+ /* Pass EXISTS conditions to upper level */
+ if (additional_conds_o != NIL)
+ {
+ Assert(*additional_conds == NIL);
+ *additional_conds = additional_conds_o;
+ }
return;
}
}
@@ -1790,7 +1843,54 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
initStringInfo(&join_sql_i);
deparseRangeTblRef(&join_sql_i, root, innerrel,
fpinfo->make_innerrel_subquery,
- ignore_rel, ignore_conds, params_list);
+ ignore_rel, ignore_conds, &additional_conds_i,
+ params_list);
+
+ /*
+ * SEMI-JOIN is deparsed as the EXISTS subquery. It references
+ * outer and inner relations, so it should be evaluated as the
+ * condition in the upper-level WHERE clause. We deparse the
+ * condition and pass it to upper level callers as an
+ * additional_conds list. Upper level callers are responsible for
+ * inserting conditions from the list where appropriate.
+ */
+ if (fpinfo->jointype == JOIN_SEMI)
+ {
+ deparse_expr_cxt context;
+ StringInfoData str;
+
+ /* Construct deparsed condition from this SEMI-JOIN */
+ initStringInfo(&str);
+ appendStringInfo(&str, "EXISTS (SELECT NULL FROM %s",
+ join_sql_i.data);
+
+ context.buf = &str;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
+
+ /*
+ * Append SEMI-JOIN clauses and EXISTS conditions from lower
+ * levels to the current EXISTS subquery
+ */
+ appendWhereClause(fpinfo->joinclauses, additional_conds_i, &context);
+
+ /*
+ * EXISTS conditions, coming from lower join levels, have just
+ * been processed.
+ */
+ if (additional_conds_i != NIL)
+ {
+ list_free_deep(additional_conds_i);
+ additional_conds_i = NIL;
+ }
+
+ /* Close parentheses for EXISTS subquery */
+ appendStringInfo(&str, ")");
+
+ *additional_conds = lappend(*additional_conds, str.data);
+ }
/*
* If outer relation is the target relation, skip deparsing it.
@@ -1801,6 +1901,12 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(fpinfo->jointype == JOIN_INNER);
Assert(fpinfo->joinclauses == NIL);
appendBinaryStringInfo(buf, join_sql_i.data, join_sql_i.len);
+ /* Pass EXISTS conditions to the upper call */
+ if (additional_conds_i != NIL)
+ {
+ Assert(*additional_conds == NIL);
+ *additional_conds = additional_conds_i;
+ }
return;
}
}
@@ -1809,33 +1915,65 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
Assert(!outerrel_is_target && !innerrel_is_target);
/*
- * For a join relation FROM clause entry is deparsed as
- *
- * ((outer relation) <join type> (inner relation) ON (joinclauses))
+ * For semijoin FROM clause is deparsed as an outer relation. An inner
+ * relation and join clauses are converted to EXISTS condition and
+ * passed to the upper level.
*/
- appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
- get_jointype_name(fpinfo->jointype), join_sql_i.data);
-
- /* Append join clause; (TRUE) if no join clause */
- if (fpinfo->joinclauses)
+ if (fpinfo->jointype == JOIN_SEMI)
{
- deparse_expr_cxt context;
+ appendStringInfo(buf, "%s", join_sql_o.data);
+ }
+ else
+ {
+ /*
+ * For a join relation FROM clause, entry is deparsed as
+ *
+ * ((outer relation) <join type> (inner relation) ON
+ * (joinclauses))
+ */
+ appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
+ get_jointype_name(fpinfo->jointype), join_sql_i.data);
- context.buf = buf;
- context.foreignrel = foreignrel;
- context.scanrel = foreignrel;
- context.root = root;
- context.params_list = params_list;
+ /* Append join clause; (TRUE) if no join clause */
+ if (fpinfo->joinclauses)
+ {
+ deparse_expr_cxt context;
- appendStringInfoChar(buf, '(');
- appendConditions(fpinfo->joinclauses, &context);
+ context.buf = buf;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.root = root;
+ context.params_list = params_list;
+
+ appendStringInfoChar(buf, '(');
+ appendConditions(fpinfo->joinclauses, &context);
+ appendStringInfoChar(buf, ')');
+ }
+ else
+ appendStringInfoString(buf, "(TRUE)");
+
+ /* End the FROM clause entry. */
appendStringInfoChar(buf, ')');
}
- else
- appendStringInfoString(buf, "(TRUE)");
- /* End the FROM clause entry. */
- appendStringInfoChar(buf, ')');
+ /*
+ * Construct additional_conds to be passed to the upper caller from
+ * current level additional_conds and additional_conds, coming from
+ * inner and outer rels.
+ */
+ if (additional_conds_o != NIL)
+ {
+ *additional_conds = list_concat(*additional_conds,
+ additional_conds_o);
+ list_free(additional_conds_o);
+ }
+
+ if (additional_conds_i != NIL)
+ {
+ *additional_conds = list_concat(*additional_conds,
+ additional_conds_i);
+ list_free(additional_conds_i);
+ }
}
else
{
@@ -1863,11 +2001,13 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
/*
* Append FROM clause entry for the given relation into buf.
+ * Conditions from lower-level SEMI-JOINs are appended to additional_conds
+ * and should be added to upper level WHERE clause.
*/
static void
deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
- List **params_list)
+ List **additional_conds, List **params_list)
{
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
@@ -1925,7 +2065,8 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
else
deparseFromExprForRel(buf, root, foreignrel, true, ignore_rel,
- ignore_conds, params_list);
+ ignore_conds, additional_conds,
+ params_list);
}
/*
@@ -2148,6 +2289,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
ListCell *lc,
*lc2;
+ List *additional_conds = NIL;
/* Set up context struct for recursion */
context.root = root;
@@ -2189,17 +2331,17 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
{
List *ignore_conds = NIL;
+
appendStringInfoString(buf, " FROM ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, additional_conds, &context);
+
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
@@ -2255,6 +2397,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
List **retrieved_attrs)
{
deparse_expr_cxt context;
+ List *additional_conds = NIL;
/* Set up context struct for recursion */
context.root = root;
@@ -2274,15 +2417,14 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
appendStringInfoString(buf, " USING ");
deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
- &ignore_conds, params_list);
+ &ignore_conds, &additional_conds, params_list);
remote_conds = list_concat(remote_conds, ignore_conds);
}
- if (remote_conds)
- {
- appendStringInfoString(buf, " WHERE ");
- appendConditions(remote_conds, &context);
- }
+ appendWhereClause(remote_conds, additional_conds, &context);
+
+ if (additional_conds != NIL)
+ list_free_deep(additional_conds);
if (foreignrel->reloptkind == RELOPT_JOINREL)
deparseExplicitTargetList(returningList, true, retrieved_attrs,
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 0a5bdf8bcc0..c988745b926 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2001,23 +2001,16 @@ SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER B
Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END, r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
(4 rows)
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
----------------------------------------------------------------------------------------
- Limit
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1
- -> Merge Semi Join
- Output: t1.c1
- Merge Cond: (t1.c1 = t2.c1)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
- -> Foreign Scan on public.ft2 t2
- Output: t2.c1
- Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
-(11 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1" FROM "S 1"."T 1" r1 WHERE EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r2."C 1" = r1."C 1"))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint OFFSET 100::bigint
+(4 rows)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
c1
@@ -4056,23 +4049,13 @@ RESET enable_sort;
-- subquery using immutable function (can be sent to remote)
PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c5) = '1970-01-17'::date) ORDER BY c1;
EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st3(10, 20);
- QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
- Sort
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Sort Key: t1.c1
- -> Nested Loop Semi Join
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Join Filter: (t2.c3 = t1.c3)
- -> Foreign Scan on public.ft1 t1
- Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
- Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 20))
- -> Materialize
- Output: t2.c3
- -> Foreign Scan on public.ft2 t2
- Output: t2.c3
- Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE (("C 1" > 10)) AND ((date(c5) = '1970-01-17'::date))
-(14 rows)
+ Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" < 20)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r3."C 1" > 10)) AND ((date(r3.c5) = '1970-01-17'::date)) AND ((r3.c3 = r1.c3))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
EXECUTE st3(10, 20);
c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
@@ -4616,6 +4599,283 @@ explain (verbose, costs off) select * from ft3 f, loct3 l
Index Cond: (l.f1 = 'foo'::text)
(12 rows)
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r2.c1, r2.c2, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 900)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r4 WHERE ((r1.c2 = r4.c1))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r4.c1, r4.c2, r4.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r4 ON (((r1.c2 = r4.c1)) AND ((r1."C 1" > 900)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1))) ORDER BY r1."C 1" ASC NULLS LAST
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 916 | 6 | 00916 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 926 | 6 | 00926 | Tue Jan 27 00:00:00 1970 PST | Tue Jan 27 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 936 | 6 | 00936 | Fri Feb 06 00:00:00 1970 PST | Fri Feb 06 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 946 | 6 | 00946 | Mon Feb 16 00:00:00 1970 PST | Mon Feb 16 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 956 | 6 | 00956 | Thu Feb 26 00:00:00 1970 PST | Thu Feb 26 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 966 | 6 | 00966 | Sun Mar 08 00:00:00 1970 PST | Sun Mar 08 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 976 | 6 | 00976 | Wed Mar 18 00:00:00 1970 PST | Wed Mar 18 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 986 | 6 | 00986 | Sat Mar 28 00:00:00 1970 PST | Sat Mar 28 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 996 | 6 | 00996 | Tue Apr 07 00:00:00 1970 PST | Tue Apr 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: (public.ft2) LEFT JOIN ((public.ft4) SEMI JOIN (public.ft5))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, s6.c1, s6.c2, s6.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r4.c1, r4.c2, r4.c3 FROM "S 1"."T 3" r4 WHERE EXISTS (SELECT NULL FROM "S 1"."T 4" r5 WHERE ((r4.c1 = r5.c1)))) s6(c1, c2, c3) ON (((r1.c2 = s6.c1)))) WHERE ((r1."C 1" > 900)) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 901 | 1 | 00901 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1 | 1 | foo | | |
+ 902 | 2 | 00902 | Sat Jan 03 00:00:00 1970 PST | Sat Jan 03 00:00:00 1970 | 2 | 2 | foo | | |
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo | | |
+ 904 | 4 | 00904 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4 | foo | | |
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo | | |
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo | | |
+ 908 | 8 | 00908 | Fri Jan 09 00:00:00 1970 PST | Fri Jan 09 00:00:00 1970 | 8 | 8 | foo | | |
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo | | |
+ 910 | 0 | 00910 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0 | foo | | |
+(10 rows)
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft5)) INNER JOIN (public.ft5 ft5_1)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 4" r8 ON (((r1.c2 <= r8.c1)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r8.c1 = r9.c1))) AND EXISTS (SELECT NULL FROM "S 1"."T 4" r7 WHERE ((r6.c1 = r7.c1))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+-----+----+-------+------------------------------+--------------------------+----+------------+-----+----+----+--------
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+ 906 | 6 | 00906 | Wed Jan 07 00:00:00 1970 PST | Wed Jan 07 00:00:00 1970 | 6 | 6 | foo | 6 | 7 | AAA006
+(10 rows)
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+ Relations: (public.ft2) SEMI JOIN ((public.ft2 ft2_1) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r1."C 1" = r3."C 1")) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r4 WHERE ((r3.c2 = r4.c2)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ftupper.c1, ftupper.c2, ftupper.c3, ftupper.c4, ftupper.c5, ftupper.c6, ftupper.c7, ftupper.c8
+ Relations: (public.ft2 ftupper) SEMI JOIN ((public.ft2) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" > 900)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r2 WHERE ((r1."C 1" = r2."C 1")) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r3 WHERE ((r2.c2 = r3.c2)))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 903 | 3 | 00903 | Sun Jan 04 00:00:00 1970 PST | Sun Jan 04 00:00:00 1970 | 3 | 3 | foo
+ 905 | 5 | 00905 | Tue Jan 06 00:00:00 1970 PST | Tue Jan 06 00:00:00 1970 | 5 | 5 | foo
+ 907 | 7 | 00907 | Thu Jan 08 00:00:00 1970 PST | Thu Jan 08 00:00:00 1970 | 7 | 7 | foo
+ 909 | 9 | 00909 | Sat Jan 10 00:00:00 1970 PST | Sat Jan 10 00:00:00 1970 | 9 | 9 | foo
+ 913 | 3 | 00913 | Wed Jan 14 00:00:00 1970 PST | Wed Jan 14 00:00:00 1970 | 3 | 3 | foo
+ 915 | 5 | 00915 | Fri Jan 16 00:00:00 1970 PST | Fri Jan 16 00:00:00 1970 | 5 | 5 | foo
+ 917 | 7 | 00917 | Sun Jan 18 00:00:00 1970 PST | Sun Jan 18 00:00:00 1970 | 7 | 7 | foo
+ 919 | 9 | 00919 | Tue Jan 20 00:00:00 1970 PST | Tue Jan 20 00:00:00 1970 | 9 | 9 | foo
+ 923 | 3 | 00923 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
+ 925 | 5 | 00925 | Mon Jan 26 00:00:00 1970 PST | Mon Jan 26 00:00:00 1970 | 5 | 5 | foo
+(10 rows)
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3
+ Relations: ((((public.ft2) INNER JOIN (public.ft4)) SEMI JOIN (public.ft2 ft2_1)) INNER JOIN (public.ft2 ft2_2)) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r6.c1, r6.c2, r6.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r6 ON (((r1.c2 = r6.c1)) AND ((r1."C 1" > 900)))) INNER JOIN "S 1"."T 1" r8 ON (((r1.c2 = r8.c2)))) WHERE EXISTS (SELECT NULL FROM "S 1"."T 3" r9 WHERE ((r1.c2 = r9.c2))) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r7 WHERE ((r7.c2 = r6.c2))) ORDER BY r1."C 1" ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3
+----+----+----+----+----+----+----+----+----+----+----
+(0 rows)
+
+-- Can't push down semi-join with inner rel vars in targetlist
+EXPLAIN (verbose, costs off)
+SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
+ ft1.c1 IN (
+ SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1)
+ ORDER BY ft1.c1 LIMIT 5;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: ft1.c1
+ -> Merge Semi Join
+ Output: ft1.c1
+ Merge Cond: (ft1.c1 = ft2_1.c1)
+ -> Foreign Scan
+ Output: ft1.c1, ft2.c1
+ Relations: (public.ft1) INNER JOIN (public.ft2)
+ Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1."C 1" ASC NULLS LAST
+ -> Foreign Scan
+ Output: ft2_1.c1, ft4.c1
+ Relations: (public.ft2 ft2_1) INNER JOIN (public.ft4)
+ Remote SQL: SELECT r5."C 1", r6.c1 FROM ("S 1"."T 1" r5 INNER JOIN "S 1"."T 3" r6 ON (((r5."C 1" = r6.c1)))) ORDER BY r5."C 1" ASC NULLS LAST
+(13 rows)
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6de2bec3b7b..8970b19211c 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -779,6 +779,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
fpinfo->make_outerrel_subquery = false;
fpinfo->make_innerrel_subquery = false;
fpinfo->lower_subquery_rels = NULL;
+ fpinfo->hidden_subquery_rels = NULL;
/* Set the relation index. */
fpinfo->relation_index = baserel->relid;
}
@@ -5724,6 +5725,45 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
return commands;
}
+/*
+ * Check if reltarget is safe enough to push down semi-join. Reltarget is not
+ * safe, if it contains references to inner rel relids, which do not belong to
+ * outer rel.
+ */
+static bool
+semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, RelOptInfo *innerrel)
+{
+ List *vars;
+ ListCell *lc;
+ bool ok = true;
+
+ Assert(joinrel->reltarget);
+
+ vars = pull_var_clause((Node *) joinrel->reltarget->exprs, PVC_INCLUDE_PLACEHOLDERS);
+
+ foreach(lc, vars)
+ {
+ Var *var = (Var *) lfirst(lc);
+
+ if (!IsA(var, Var))
+ continue;
+
+ if (bms_is_member(var->varno, innerrel->relids) &&
+ !bms_is_member(var->varno, outerrel->relids))
+ {
+ /*
+ * The planner can create semi-joins, which refer to inner rel
+ * vars in its target list. However, we deparse semi-join as an
+ * exists() subquery, so can't handle references to inner rel in
+ * the target list.
+ */
+ ok = false;
+ break;
+ }
+ }
+ return ok;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -5741,12 +5781,19 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
List *joinclauses;
/*
- * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
- * Constructing queries representing SEMI and ANTI joins is hard, hence
- * not considered right now.
+ * We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
+ * Constructing queries representing ANTI joins is hard, hence not
+ * considered right now.
*/
if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
- jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+ jointype != JOIN_RIGHT && jointype != JOIN_FULL &&
+ jointype != JOIN_SEMI)
+ return false;
+
+ /*
+ * We can't push down semi-join if its reltarget is not safe
+ */
+ if ((jointype == JOIN_SEMI) && !semijoin_target_ok(root, joinrel, outerrel, innerrel))
return false;
/*
@@ -5858,6 +5905,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
fpinfo->lower_subquery_rels = bms_union(fpinfo_o->lower_subquery_rels,
fpinfo_i->lower_subquery_rels);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo_o->hidden_subquery_rels,
+ fpinfo_i->hidden_subquery_rels);
/*
* Pull the other remote conditions from the joining relations into join
@@ -5871,6 +5920,12 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
* the joinclauses, since they need to be evaluated while constructing the
* join.
*
+ * For SEMI-JOIN clauses from inner relation can not be added to
+ * remote_conds, but should be treated as join clauses (as they are
+ * deparsed to EXISTS subquery, where inner relation can be referred). A
+ * list of relation ids, which can't be referred to from higher levels, is
+ * preserved as a hidden_subquery_rels list.
+ *
* For a FULL OUTER JOIN, the other clauses from either relation can not
* be added to the joinclauses or remote_conds, since each relation acts
* as an outer relation for the other.
@@ -5901,6 +5956,16 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo_i->remote_conds);
break;
+ case JOIN_SEMI:
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo_i->remote_conds);
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ fpinfo->remote_conds);
+ fpinfo->remote_conds = list_copy(fpinfo_o->remote_conds);
+ fpinfo->hidden_subquery_rels = bms_union(fpinfo->hidden_subquery_rels,
+ innerrel->relids);
+ break;
+
case JOIN_FULL:
/*
@@ -5943,6 +6008,24 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
fpinfo->joinclauses = fpinfo->remote_conds;
fpinfo->remote_conds = NIL;
}
+ else if (jointype == JOIN_LEFT || jointype == JOIN_RIGHT || jointype == JOIN_FULL)
+ {
+ /*
+ * Conditions, generated from semi-joins, should be evaluated before
+ * LEFT/RIGHT/FULL join.
+ */
+ if (!bms_is_empty(fpinfo_o->hidden_subquery_rels))
+ {
+ fpinfo->make_outerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, outerrel->relids);
+ }
+
+ if (!bms_is_empty(fpinfo_i->hidden_subquery_rels))
+ {
+ fpinfo->make_innerrel_subquery = true;
+ fpinfo->lower_subquery_rels = bms_add_members(fpinfo->lower_subquery_rels, innerrel->relids);
+ }
+ }
/* Mark that this join can be pushed down safely */
fpinfo->pushdown_safe = true;
@@ -7692,6 +7775,8 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
{
ListCell *lc;
+ PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
+
foreach(lc, ec->ec_members)
{
EquivalenceMember *em = (EquivalenceMember *) lfirst(lc);
@@ -7702,6 +7787,7 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
*/
if (bms_is_subset(em->em_relids, rel->relids) &&
!bms_is_empty(em->em_relids) &&
+ bms_is_empty(bms_intersect(em->em_relids, fpinfo->hidden_subquery_rels)) &&
is_foreign_expr(root, rel, em->em_expr))
return em;
}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 47157ac887e..9a1a80aebb4 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -118,6 +118,10 @@ typedef struct PgFdwRelationInfo
* subquery? */
Relids lower_subquery_rels; /* all relids appearing in lower
* subqueries */
+ Relids hidden_subquery_rels; /* relids, which can't be referred to
+ * from upper relations, used
+ * internally for equivalence member
+ * search */
/*
* Index of the relation. It is used to create an alias to a subquery
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 075da4ff867..cb405407028 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -600,7 +600,7 @@ WITH t (c1_1, c1_3, c2_1) AS MATERIALIZED (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t
-- ctid with whole-row reference
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- SEMI JOIN, not pushed down
+-- SEMI JOIN
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
@@ -1305,6 +1305,130 @@ explain (verbose, costs off) select * from ft3 where f2 = 'foo' COLLATE "C";
explain (verbose, costs off) select * from ft3 f, loct3 l
where f.f3 = l.f3 COLLATE "POSIX" and l.f1 = 'foo';
+-- ===================================================================
+-- test SEMI-JOIN pushdown
+-- ===================================================================
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN ft4 ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ AND EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)
+ ORDER BY ft2.c1;
+
+-- The same query, different join order
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1;
+
+-- Left join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 LEFT JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Several semi-joins per upper level join
+EXPLAIN (verbose, costs off)
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE
+ EXISTS (SELECT 1 FROM ft5 WHERE ft4.c1 = ft5.c1)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN (SELECT * FROM ft5 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft5.c1)) ft5
+ ON ft2.c2 <= ft5.c1
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Semi-join below Semi-join
+EXPLAIN (verbose, costs off)
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.* FROM ft2 WHERE
+ c1 = ANY (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2))
+ AND ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Upper level relations shouldn't refer EXISTS() subqueries
+EXPLAIN (verbose, costs off)
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+SELECT * FROM ft2 ftupper WHERE
+ EXISTS (
+ SELECT c1 FROM ft2 WHERE
+ EXISTS (SELECT 1 FROM ft4 WHERE ft4.c2 = ft2.c2) AND c1 = ftupper.c1 )
+ AND ftupper.c1 > 900
+ ORDER BY ftupper.c1 LIMIT 10;
+
+-- EXISTS should be propogated to the highest upper inner join
+EXPLAIN (verbose, costs off)
+ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
+ (SELECT * FROM ft4 WHERE EXISTS (
+ SELECT 1 FROM ft2 WHERE ft2.c2 = ft4.c2)) ft4
+ ON ft2.c2 = ft4.c1
+ INNER JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (
+ SELECT 1 FROM ft4 WHERE ft2.c2 = ft4.c2)) ft21
+ ON ft2.c2 = ft21.c2
+ WHERE ft2.c1 > 900
+ ORDER BY ft2.c1 LIMIT 10;
+
+-- Can't push down semi-join with inner rel vars in targetlist
+EXPLAIN (verbose, costs off)
+SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
+ ft1.c1 IN (
+ SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1)
+ ORDER BY ft1.c1 LIMIT 5;
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
--
2.39.3 (Apple Git-145)
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-18 00:20 Robins Tharakan <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Robins Tharakan @ 2025-03-18 00:20 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Alexander Pyhalov <[email protected]>; Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected] <[email protected]>
Hi Alexander,
On Mon, 4 Dec 2023 at 07:22, Alexander Korotkov <[email protected]>
wrote:
>
>
> Now, I think this looks good. I'm going to push this if no objections.
After this commit, I began seeing an unexpected ERROR - see this bug-report.
https://www.postgresql.org/message-id/18852-fb75b88160678f78%40postgresql.org
-
robins
https://robins.in
Ref:
1.
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=824dbea3e41efa3b35094163c834988de...
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-18 00:27 Alexander Korotkov <[email protected]>
parent: Robins Tharakan <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Korotkov @ 2025-03-18 00:27 UTC (permalink / raw)
To: Robins Tharakan <[email protected]>; +Cc: Alexander Pyhalov <[email protected]>; Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected] <[email protected]>
Hi, Robins!
On Tue, Mar 18, 2025 at 2:20 AM Robins Tharakan <[email protected]> wrote:
> On Mon, 4 Dec 2023 at 07:22, Alexander Korotkov <[email protected]> wrote:
> >
> >
> > Now, I think this looks good. I'm going to push this if no objections.
>
> After this commit, I began seeing an unexpected ERROR - see this bug-report.
> https://www.postgresql.org/message-id/18852-fb75b88160678f78%40postgresql.org
Thank you for pointing.
I'll check this in the next couple of days.
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-18 11:13 Alexander Pyhalov <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Pyhalov @ 2025-03-18 11:13 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Robins Tharakan <[email protected]>; Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected]
Alexander Korotkov писал(а) 2025-03-18 03:27:
> Hi, Robins!
>
> On Tue, Mar 18, 2025 at 2:20 AM Robins Tharakan <[email protected]>
> wrote:
>> On Mon, 4 Dec 2023 at 07:22, Alexander Korotkov <[email protected]>
>> wrote:
>> >
>> >
>> > Now, I think this looks good. I'm going to push this if no objections.
>>
>> After this commit, I began seeing an unexpected ERROR - see this
>> bug-report.
>> https://www.postgresql.org/message-id/18852-fb75b88160678f78%40postgresql.org
>
> Thank you for pointing.
> I'll check this in the next couple of days.
>
Hi.
It seems conditions, coming from semi-joins, are handled incorrectly
under left and right join. When deparsing left/right joins and there are
semi-joins in inner or outer part of the query, the corresponding part
is deparsed as subquery. And we can't refer subquery vars from above, so
just should not pull up the restrictinfos. The attached patch does
exactly this.
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] 0001-Avoid-pulling-up-restrict-infos-from-subqueries.patch (5.4K, ../../[email protected]/2-0001-Avoid-pulling-up-restrict-infos-from-subqueries.patch)
download | inline diff:
From 04c89be4b2802a0b51b10ca4828108a338c77e71 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Tue, 18 Mar 2025 13:46:08 +0300
Subject: [PATCH] Avoid pulling up restrict infos from subqueries
Semi-join joins below left/right join are deparsed as
subqueries. We can't refer to subqueries vars from upper relations,
so avoid pulling conditions from them.
---
.../postgres_fdw/expected/postgres_fdw.out | 39 +++++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 22 +++++++++--
contrib/postgres_fdw/sql/postgres_fdw.sql | 18 +++++++++
3 files changed, 75 insertions(+), 4 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index bb4ed3059c4..88385e06f31 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -4936,6 +4936,45 @@ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
----+----+----+----+----+----+----+----+----+----+----
(0 rows)
+-- semi-join conditions shouldn't pop up as
+-- left/right join join clause
+set enable_material to off;
+EXPLAIN (verbose, costs off)
+ SELECT NULL FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft2.c1 < 10))
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft2.c1 < 10))
+ ON (TRUE)
+LIMIT 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: NULL::text
+ Relations: ((public.ft2 ft2_1) SEMI JOIN (public.ft4 ft4_1)) LEFT JOIN ((public.ft2) SEMI JOIN (public.ft4))
+ Remote SQL: SELECT NULL FROM ((SELECT NULL FROM "S 1"."T 1" r6 WHERE ((r6."C 1" < 10)) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r7)) s8 LEFT JOIN (SELECT NULL FROM "S 1"."T 1" r4 WHERE ((r4."C 1" < 10)) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r5)) s9 ON (TRUE)) LIMIT 10::bigint
+(4 rows)
+
+SELECT NULL FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft2.c1 < 10))
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft2.c1 < 10))
+ ON (TRUE)
+LIMIT 10;
+ ?column?
+----------
+
+
+
+
+
+
+
+
+
+
+(10 rows)
+
+reset enable_material;
-- Can't push down semi-join with inner rel vars in targetlist
EXPLAIN (verbose, costs off)
SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 1131a8bf77e..8af8b802a24 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -5964,16 +5964,30 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
break;
case JOIN_LEFT:
- fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ /*
+ * When semi-join is involved in inner or outer part of left join,
+ * it's deparsed as a subquery and we can't refer to its vars
+ * on upper level
+ */
+ if (bms_is_empty(fpinfo_i->hidden_subquery_rels))
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
fpinfo_i->remote_conds);
- fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
+ if (bms_is_empty(fpinfo_o->hidden_subquery_rels))
+ fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
fpinfo_o->remote_conds);
break;
case JOIN_RIGHT:
- fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ /*
+ * When semi-join is involved in inner or outer part of right join,
+ * it's deparsed as a subquery and we can't refer to its vars
+ * on upper level
+ */
+ if (bms_is_empty(fpinfo_o->hidden_subquery_rels))
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
fpinfo_o->remote_conds);
- fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
+ if (bms_is_empty(fpinfo_i->hidden_subquery_rels))
+ fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
fpinfo_i->remote_conds);
break;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index d45e9f8ab52..faedeab9d41 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1455,6 +1455,24 @@ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
WHERE ft2.c1 > 900
ORDER BY ft2.c1 LIMIT 10;
+-- semi-join conditions shouldn't pop up as
+-- left/right join join clause
+set enable_material to off;
+EXPLAIN (verbose, costs off)
+ SELECT NULL FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft2.c1 < 10))
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft2.c1 < 10))
+ ON (TRUE)
+LIMIT 10;
+SELECT NULL FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft2.c1 < 10))
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft2.c1 < 10))
+ ON (TRUE)
+LIMIT 10;
+reset enable_material;
+
-- Can't push down semi-join with inner rel vars in targetlist
EXPLAIN (verbose, costs off)
SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
--
2.43.0
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-18 11:19 Alexander Korotkov <[email protected]>
parent: Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Korotkov @ 2025-03-18 11:19 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: Robins Tharakan <[email protected]>; Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected]
Hi, Alexander!
On Tue, Mar 18, 2025 at 1:13 PM Alexander Pyhalov
<[email protected]> wrote:
> Alexander Korotkov писал(а) 2025-03-18 03:27:
> > Hi, Robins!
> >
> > On Tue, Mar 18, 2025 at 2:20 AM Robins Tharakan <[email protected]>
> > wrote:
> >> On Mon, 4 Dec 2023 at 07:22, Alexander Korotkov <[email protected]>
> >> wrote:
> >> >
> >> >
> >> > Now, I think this looks good. I'm going to push this if no objections.
> >>
> >> After this commit, I began seeing an unexpected ERROR - see this
> >> bug-report.
> >> https://www.postgresql.org/message-id/18852-fb75b88160678f78%40postgresql.org
> >
> > Thank you for pointing.
> > I'll check this in the next couple of days.
> >
>
> It seems conditions, coming from semi-joins, are handled incorrectly
> under left and right join. When deparsing left/right joins and there are
> semi-joins in inner or outer part of the query, the corresponding part
> is deparsed as subquery. And we can't refer subquery vars from above, so
> just should not pull up the restrictinfos. The attached patch does
> exactly this.
Thank you for the fix. It looks like this fix generally affects
left/right joins, not just semi-joins. Can you confirm this doesn't
cause regression for other cases?
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-18 16:04 Alexander Pyhalov <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Pyhalov @ 2025-03-18 16:04 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Robins Tharakan <[email protected]>; Tomas Vondra <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected]
Alexander Korotkov писал(а) 2025-03-18 14:19:
> Hi, Alexander!
>
> On Tue, Mar 18, 2025 at 1:13 PM Alexander Pyhalov
> <[email protected]> wrote:
>> Alexander Korotkov писал(а) 2025-03-18 03:27:
>> > Hi, Robins!
>> >
>> > On Tue, Mar 18, 2025 at 2:20 AM Robins Tharakan <[email protected]>
>> > wrote:
>> >> On Mon, 4 Dec 2023 at 07:22, Alexander Korotkov <[email protected]>
>> >> wrote:
>> >> >
>> >> >
>> >> > Now, I think this looks good. I'm going to push this if no objections.
>> >>
>> >> After this commit, I began seeing an unexpected ERROR - see this
>> >> bug-report.
>> >> https://www.postgresql.org/message-id/18852-fb75b88160678f78%40postgresql.org
>> >
>> > Thank you for pointing.
>> > I'll check this in the next couple of days.
>> >
>>
>> It seems conditions, coming from semi-joins, are handled incorrectly
>> under left and right join. When deparsing left/right joins and there
>> are
>> semi-joins in inner or outer part of the query, the corresponding part
>> is deparsed as subquery. And we can't refer subquery vars from above,
>> so
>> just should not pull up the restrictinfos. The attached patch does
>> exactly this.
>
> Thank you for the fix. It looks like this fix generally affects
> left/right joins, not just semi-joins. Can you confirm this doesn't
> cause regression for other cases?
>
Hi.
This shouldn't. When semi-join is found below left/right join, it's
deparsed as subquery.
Interesting enough, this mechanics (deparsing as subquery) is used
1) for semi-joins under left/right join,
2) for full outer joins when inner or outer part has some remote_conds.
The issue here is that after subquery is deparsed, we don't consider if
its target attributes are available to the upper level
join . As for semi-join itself, all conditions are still deparsed on
left/right join boundary, they are just not propagated further.
This shouldn't be a problem, as they are evaluated in subquery. As for
left/right join without semi-join beneath it - its behavior is not
affected
(as hidden_subquery_rels is empty).
--
Best regards,
Alexander Pyhalov,
Postgres Professional
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-24 01:21 Alexander Korotkov <[email protected]>
parent: Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Korotkov @ 2025-03-24 01:21 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: Robins Tharakan <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected]
Hi, Alexander!
On Tue, Mar 18, 2025 at 6:04 PM Alexander Pyhalov
<[email protected]> wrote:
> This shouldn't. When semi-join is found below left/right join, it's
> deparsed as subquery.
> Interesting enough, this mechanics (deparsing as subquery) is used
> 1) for semi-joins under left/right join,
> 2) for full outer joins when inner or outer part has some remote_conds.
>
> The issue here is that after subquery is deparsed, we don't consider if
> its target attributes are available to the upper level
> join . As for semi-join itself, all conditions are still deparsed on
> left/right join boundary, they are just not propagated further.
> This shouldn't be a problem, as they are evaluated in subquery. As for
> left/right join without semi-join beneath it - its behavior is not
> affected
> (as hidden_subquery_rels is empty).
Thank you for the explanation. But I have another question. Aren't
the checks you've proposed too strict? hidden_subquery_rels are
propagated all the way to the join tree. So, pulling conditions would
be disables all the way to the join tree too. Is it enough to just
disable pulling conditions directly from semi-joins, then their
further pulls will be disabled automatically? See the attached patch.
It also contains other (mostly cosmetic improvements).
------
Regards,
Alexander Korotkov
Supabase
Attachments:
[application/octet-stream] v2-0001-Avoid-pulling-up-restrict-infos-from-subqueries.patch (6.2K, ../../CAPpHfdu0+Zyvy6acCeW2YKaaTbH=PggyNJbrKMu2__KbEGVEFA@mail.gmail.com/2-v2-0001-Avoid-pulling-up-restrict-infos-from-subqueries.patch)
download | inline diff:
From 510031bfa9eba3726cc93ddf05c1d9217b4e1585 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Tue, 18 Mar 2025 13:46:08 +0300
Subject: [PATCH v2] Avoid pulling up restrict infos from subqueries
Semi-join joins below left/right join are deparsed as subqueries. Thus,
we can't refer to subqueries vars from upper relations. This commit avoids
pulling conditions from them.
Reported-by: Robins Tharakan <[email protected]>
Bug: #18852
Discussion: https://postgr.es/m/CAEP4nAzryLd3gwcUpFBAG9MWyDfMRX8ZjuyY2XXjyC_C6k%2B_Zw%40mail.gmail.com
Author: Alexander Pyhalov <[email protected]>
Reviewed-by: Alexander Korotkov <[email protected]>
Backpatch-through: 17
---
.../postgres_fdw/expected/postgres_fdw.out | 38 +++++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 22 +++++++++--
contrib/postgres_fdw/sql/postgres_fdw.sql | 17 +++++++++
3 files changed, 73 insertions(+), 4 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index bb4ed3059c4..d1acee5a5fa 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -4936,6 +4936,44 @@ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
----+----+----+----+----+----+----+----+----+----+----
(0 rows)
+-- Semi-join conditions shouldn't pop up as left/right join clauses.
+SET enable_material TO off;
+EXPLAIN (verbose, costs off)
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ORDER BY x1.c1 LIMIT 10;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1
+ Relations: ((public.ft2 ft2_1) LEFT JOIN ((public.ft2) SEMI JOIN (public.ft4))) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT s9.c1 FROM ("S 1"."T 1" r6 LEFT JOIN (SELECT r4."C 1" FROM "S 1"."T 1" r4 WHERE ((r4.c2 < 10)) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r5 WHERE ((r4."C 1" = r5.c1)))) s9(c1) ON (((s9.c1 = r6."C 1")))) WHERE ((r6.c2 < 10)) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r7 WHERE ((r6."C 1" = r7.c1))) ORDER BY s9.c1 ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ORDER BY x1.c1 LIMIT 10;
+ c1
+----
+ 2
+ 4
+ 6
+ 8
+ 10
+ 12
+ 14
+ 16
+ 18
+ 20
+(10 rows)
+
+RESET enable_material;
-- Can't push down semi-join with inner rel vars in targetlist
EXPLAIN (verbose, costs off)
SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6beae0fa37f..f9bc7b60939 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -5963,16 +5963,30 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
break;
case JOIN_LEFT:
- fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ /*
+ * When semi-join is involved in the inner or outer part of the
+ * left join, it's deparsed as a subquery, and we can't refer to
+ * its vars on the upper level.
+ */
+ if (fpinfo_i->jointype != JOIN_SEMI)
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
fpinfo_i->remote_conds);
- fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
+ if (fpinfo_o->jointype != JOIN_SEMI)
+ fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
fpinfo_o->remote_conds);
break;
case JOIN_RIGHT:
- fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ /*
+ * When semi-join is involved in the inner or outer part of the
+ * right join, it's deparsed as a subquery, and we can't refer to
+ * its vars on the upper level.
+ */
+ if (fpinfo_o->jointype != JOIN_SEMI)
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
fpinfo_o->remote_conds);
- fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
+ if (fpinfo_i->jointype != JOIN_SEMI)
+ fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
fpinfo_i->remote_conds);
break;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index d45e9f8ab52..ea6287b03fd 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1455,6 +1455,23 @@ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
WHERE ft2.c1 > 900
ORDER BY ft2.c1 LIMIT 10;
+-- Semi-join conditions shouldn't pop up as left/right join clauses.
+SET enable_material TO off;
+EXPLAIN (verbose, costs off)
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ORDER BY x1.c1 LIMIT 10;
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ORDER BY x1.c1 LIMIT 10;
+RESET enable_material;
+
-- Can't push down semi-join with inner rel vars in targetlist
EXPLAIN (verbose, costs off)
SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-24 07:07 Alexander Pyhalov <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Pyhalov @ 2025-03-24 07:07 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Robins Tharakan <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected]
Alexander Korotkov писал(а) 2025-03-24 04:21:
> Hi, Alexander!
>
> On Tue, Mar 18, 2025 at 6:04 PM Alexander Pyhalov
> <[email protected]> wrote:
>> This shouldn't. When semi-join is found below left/right join, it's
>> deparsed as subquery.
>> Interesting enough, this mechanics (deparsing as subquery) is used
>> 1) for semi-joins under left/right join,
>> 2) for full outer joins when inner or outer part has some
>> remote_conds.
>>
>> The issue here is that after subquery is deparsed, we don't consider
>> if
>> its target attributes are available to the upper level
>> join . As for semi-join itself, all conditions are still deparsed on
>> left/right join boundary, they are just not propagated further.
>> This shouldn't be a problem, as they are evaluated in subquery. As for
>> left/right join without semi-join beneath it - its behavior is not
>> affected
>> (as hidden_subquery_rels is empty).
>
> Thank you for the explanation. But I have another question. Aren't
> the checks you've proposed too strict? hidden_subquery_rels are
> propagated all the way to the join tree. So, pulling conditions would
> be disables all the way to the join tree too. Is it enough to just
> disable pulling conditions directly from semi-joins, then their
> further pulls will be disabled automatically? See the attached patch.
> It also contains other (mostly cosmetic improvements).
>
> ------
> Regards,
> Alexander Korotkov
> Supabase
Hi. No, they are not too strict. Look at the following example
EXPLAIN (verbose, costs off)
SELECT x1.c1 FROM
(SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE
ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
RIGHT JOIN
(SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE
ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
ON (x1.c1 = x2.c1)
LEFT JOIN
(SELECT * FROM ft2 WHERE c2 < 11) x3
ON (x1.c1 = x3.c1)
ORDER BY x1.c1 LIMIT 10;
With patch which you suggest, we'll deparse left part of left join as
subquery, but will try to pop c2 < 10 condition from
(8) LEFT JOIN ((6) SEMI JOIN (7)) subquery. When we look at left join of
this subquery and ft2, we still deparse left part as
subquery, so can't pop up conditions from it.
--
Best regards,
Alexander Pyhalov,
Postgres Professional
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-24 08:49 Alexander Korotkov <[email protected]>
parent: Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Alexander Korotkov @ 2025-03-24 08:49 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: Robins Tharakan <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected]
On Mon, Mar 24, 2025 at 9:07 AM Alexander Pyhalov
<[email protected]> wrote:
> Alexander Korotkov писал(а) 2025-03-24 04:21:
> > Hi, Alexander!
> >
> > On Tue, Mar 18, 2025 at 6:04 PM Alexander Pyhalov
> > <[email protected]> wrote:
> >> This shouldn't. When semi-join is found below left/right join, it's
> >> deparsed as subquery.
> >> Interesting enough, this mechanics (deparsing as subquery) is used
> >> 1) for semi-joins under left/right join,
> >> 2) for full outer joins when inner or outer part has some
> >> remote_conds.
> >>
> >> The issue here is that after subquery is deparsed, we don't consider
> >> if
> >> its target attributes are available to the upper level
> >> join . As for semi-join itself, all conditions are still deparsed on
> >> left/right join boundary, they are just not propagated further.
> >> This shouldn't be a problem, as they are evaluated in subquery. As for
> >> left/right join without semi-join beneath it - its behavior is not
> >> affected
> >> (as hidden_subquery_rels is empty).
> >
> > Thank you for the explanation. But I have another question. Aren't
> > the checks you've proposed too strict? hidden_subquery_rels are
> > propagated all the way to the join tree. So, pulling conditions would
> > be disables all the way to the join tree too. Is it enough to just
> > disable pulling conditions directly from semi-joins, then their
> > further pulls will be disabled automatically? See the attached patch.
> > It also contains other (mostly cosmetic improvements).
> >
> > ------
> > Regards,
> > Alexander Korotkov
> > Supabase
>
> Hi. No, they are not too strict. Look at the following example
>
> EXPLAIN (verbose, costs off)
> SELECT x1.c1 FROM
> (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE
> ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
> RIGHT JOIN
> (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE
> ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
> ON (x1.c1 = x2.c1)
> LEFT JOIN
> (SELECT * FROM ft2 WHERE c2 < 11) x3
> ON (x1.c1 = x3.c1)
> ORDER BY x1.c1 LIMIT 10;
>
> With patch which you suggest, we'll deparse left part of left join as
> subquery, but will try to pop c2 < 10 condition from
> (8) LEFT JOIN ((6) SEMI JOIN (7)) subquery. When we look at left join of
> this subquery and ft2, we still deparse left part as
> subquery, so can't pop up conditions from it.
I've checked, this query seems to result in the exactly same remote
SQLs with your and mine patches. Could you elaborate more on the
difference? Do you think foreign_join_ok() can give different results
on this query?
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-24 16:56 Alexander Pyhalov <[email protected]>
parent: Alexander Korotkov <[email protected]>
0 siblings, 2 replies; 19+ messages in thread
From: Alexander Pyhalov @ 2025-03-24 16:56 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Robins Tharakan <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected]
Alexander Korotkov писал(а) 2025-03-24 11:49:
> On Mon, Mar 24, 2025 at 9:07 AM Alexander Pyhalov
> <[email protected]> wrote:
>> Alexander Korotkov писал(а) 2025-03-24 04:21:
>> > Hi, Alexander!
>> >
>> > On Tue, Mar 18, 2025 at 6:04 PM Alexander Pyhalov
>> > <[email protected]> wrote:
>> >> This shouldn't. When semi-join is found below left/right join, it's
>> >> deparsed as subquery.
>> >> Interesting enough, this mechanics (deparsing as subquery) is used
>> >> 1) for semi-joins under left/right join,
>> >> 2) for full outer joins when inner or outer part has some
>> >> remote_conds.
>> >>
>> >> The issue here is that after subquery is deparsed, we don't consider
>> >> if
>> >> its target attributes are available to the upper level
>> >> join . As for semi-join itself, all conditions are still deparsed on
>> >> left/right join boundary, they are just not propagated further.
>> >> This shouldn't be a problem, as they are evaluated in subquery. As for
>> >> left/right join without semi-join beneath it - its behavior is not
>> >> affected
>> >> (as hidden_subquery_rels is empty).
>> >
>> > Thank you for the explanation. But I have another question. Aren't
>> > the checks you've proposed too strict? hidden_subquery_rels are
>> > propagated all the way to the join tree. So, pulling conditions would
>> > be disables all the way to the join tree too. Is it enough to just
>> > disable pulling conditions directly from semi-joins, then their
>> > further pulls will be disabled automatically? See the attached patch.
>> > It also contains other (mostly cosmetic improvements).
>> >
>> > ------
>> > Regards,
>> > Alexander Korotkov
>> > Supabase
>>
>> Hi. No, they are not too strict. Look at the following example
>>
>> EXPLAIN (verbose, costs off)
>> SELECT x1.c1 FROM
>> (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4
>> WHERE
>> ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
>> RIGHT JOIN
>> (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4
>> WHERE
>> ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
>> ON (x1.c1 = x2.c1)
>> LEFT JOIN
>> (SELECT * FROM ft2 WHERE c2 < 11) x3
>> ON (x1.c1 = x3.c1)
>> ORDER BY x1.c1 LIMIT 10;
>>
>> With patch which you suggest, we'll deparse left part of left join as
>> subquery, but will try to pop c2 < 10 condition from
>> (8) LEFT JOIN ((6) SEMI JOIN (7)) subquery. When we look at left join
>> of
>> this subquery and ft2, we still deparse left part as
>> subquery, so can't pop up conditions from it.
>
> I've checked, this query seems to result in the exactly same remote
> SQLs with your and mine patches. Could you elaborate more on the
> difference? Do you think foreign_join_ok() can give different results
> on this query?
Hi.
With your patch this example gives the same
ERROR: unexpected expression in subquery output
This happens, because we don't keep knowledge that we have deparsed all
semi-joins below this left join. As long as left/right join has
semi-join in its left or right part, this part will be deparsed as
subquery (look at the following lines in foreign_join_ok()):
else if (jointype == JOIN_LEFT || jointype == JOIN_RIGHT ||
jointype == JOIN_FULL)
{
/*
* Conditions, generated from semi-joins, should be
evaluated before
* LEFT/RIGHT/FULL join.
*/
if (!bms_is_empty(fpinfo_o->hidden_subquery_rels))
{
fpinfo->make_outerrel_subquery = true;
fpinfo->lower_subquery_rels =
bms_add_members(fpinfo->lower_subquery_rels, outerrel->relids);
}
if (!bms_is_empty(fpinfo_i->hidden_subquery_rels))
{
fpinfo->make_innerrel_subquery = true;
fpinfo->lower_subquery_rels =
bms_add_members(fpinfo->lower_subquery_rels, innerrel->relids);
}
}
So, we still can't refer to its remote_conds from upper level queries
(as not all Vars are available from subquery after subquery is created
in one part of left or right join). It's not necessary to have semi-join
for this as immediate left/right join inner or outer for inner/outer to
be deparsed as subquery. But it shouldn't be an issue - we've already
used remote_conds when created this subquery.
What I'm trying to say - logic of 'making subquery' and extracting
conditions should match (or we need more sophisticated way of forming
subquery targetlist, so that extracted conditions could be used above
subqueries).
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] example.diff (5.6K, ../../[email protected]/2-example.diff)
download | inline diff:
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index bb4ed3059c4..d1acee5a5fa 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -4936,6 +4936,44 @@ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
----+----+----+----+----+----+----+----+----+----+----
(0 rows)
+-- Semi-join conditions shouldn't pop up as left/right join clauses.
+SET enable_material TO off;
+EXPLAIN (verbose, costs off)
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ORDER BY x1.c1 LIMIT 10;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: ft2.c1
+ Relations: ((public.ft2 ft2_1) LEFT JOIN ((public.ft2) SEMI JOIN (public.ft4))) SEMI JOIN (public.ft4 ft4_1)
+ Remote SQL: SELECT s9.c1 FROM ("S 1"."T 1" r6 LEFT JOIN (SELECT r4."C 1" FROM "S 1"."T 1" r4 WHERE ((r4.c2 < 10)) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r5 WHERE ((r4."C 1" = r5.c1)))) s9(c1) ON (((s9.c1 = r6."C 1")))) WHERE ((r6.c2 < 10)) AND EXISTS (SELECT NULL FROM "S 1"."T 3" r7 WHERE ((r6."C 1" = r7.c1))) ORDER BY s9.c1 ASC NULLS LAST LIMIT 10::bigint
+(4 rows)
+
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ORDER BY x1.c1 LIMIT 10;
+ c1
+----
+ 2
+ 4
+ 6
+ 8
+ 10
+ 12
+ 14
+ 16
+ 18
+ 20
+(10 rows)
+
+RESET enable_material;
-- Can't push down semi-join with inner rel vars in targetlist
EXPLAIN (verbose, costs off)
SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 1131a8bf77e..455ddf101b4 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -5964,16 +5964,30 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
break;
case JOIN_LEFT:
- fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ /*
+ * When semi-join is involved in the inner or outer part of the
+ * left join, it's deparsed as a subquery, and we can't refer to
+ * its vars on the upper level.
+ */
+ if (fpinfo_i->jointype != JOIN_SEMI)
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
fpinfo_i->remote_conds);
- fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
+ if (fpinfo_o->jointype != JOIN_SEMI)
+ fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
fpinfo_o->remote_conds);
break;
case JOIN_RIGHT:
- fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
+ /*
+ * When semi-join is involved in the inner or outer part of the
+ * right join, it's deparsed as a subquery, and we can't refer to
+ * its vars on the upper level.
+ */
+ if (fpinfo_o->jointype != JOIN_SEMI)
+ fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
fpinfo_o->remote_conds);
- fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
+ if (fpinfo_i->jointype != JOIN_SEMI)
+ fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
fpinfo_i->remote_conds);
break;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index d45e9f8ab52..c58e8fc9912 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1455,6 +1455,34 @@ SELECT ft2.*, ft4.* FROM ft2 INNER JOIN
WHERE ft2.c1 > 900
ORDER BY ft2.c1 LIMIT 10;
+-- Semi-join conditions shouldn't pop up as left/right join clauses.
+SET enable_material TO off;
+EXPLAIN (verbose, costs off)
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ORDER BY x1.c1 LIMIT 10;
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ORDER BY x1.c1 LIMIT 10;
+
+EXPLAIN (verbose, costs off)
+SELECT x1.c1 FROM
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
+ RIGHT JOIN
+ (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4 WHERE ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
+ ON (x1.c1 = x2.c1)
+ LEFT JOIN
+ (SELECT * FROM ft2 WHERE c2 < 11) x3
+ ON (x1.c1 = x3.c1)
+ORDER BY x1.c1 LIMIT 10;
+RESET enable_material;
+
-- Can't push down semi-join with inner rel vars in targetlist
EXPLAIN (verbose, costs off)
SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-25 07:04 Tender Wang <[email protected]>
parent: Alexander Pyhalov <[email protected]>
1 sibling, 0 replies; 19+ messages in thread
From: Tender Wang @ 2025-03-25 07:04 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: Alexander Pyhalov <[email protected]>; Robins Tharakan <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>
Hi Alexander Korotkov,
I saw the Bug: #18852 fix had been pushed. While I debug the sem- join
pushed down codes,
I wrote a patch to remove an unnecessary check on semijoin_target_ok() in
[1]. The patch
has been reviewed by Alexander Pyhalov. I think it's ready to commit.
Any thoughts?
[1]
https://www.postgresql.org/message-id/flat/CAHewXN=8aW4hd_W71F7Ua4+_w0=bppuvvTEBFBF6G0NuSXLwUw@mail....
--
Thanks,
Tender Wang
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Add semi-join pushdown to postgres_fdw
@ 2025-03-25 10:55 Alexander Korotkov <[email protected]>
parent: Alexander Pyhalov <[email protected]>
1 sibling, 0 replies; 19+ messages in thread
From: Alexander Korotkov @ 2025-03-25 10:55 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: Robins Tharakan <[email protected]>; [email protected] <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers; Ian Lawrence Barwick <[email protected]>; [email protected]
On Mon, Mar 24, 2025 at 6:56 PM Alexander Pyhalov
<[email protected]> wrote:
> Alexander Korotkov писал(а) 2025-03-24 11:49:
> > On Mon, Mar 24, 2025 at 9:07 AM Alexander Pyhalov
> > <[email protected]> wrote:
> >> Alexander Korotkov писал(а) 2025-03-24 04:21:
> >> > Hi, Alexander!
> >> >
> >> > On Tue, Mar 18, 2025 at 6:04 PM Alexander Pyhalov
> >> > <[email protected]> wrote:
> >> >> This shouldn't. When semi-join is found below left/right join, it's
> >> >> deparsed as subquery.
> >> >> Interesting enough, this mechanics (deparsing as subquery) is used
> >> >> 1) for semi-joins under left/right join,
> >> >> 2) for full outer joins when inner or outer part has some
> >> >> remote_conds.
> >> >>
> >> >> The issue here is that after subquery is deparsed, we don't consider
> >> >> if
> >> >> its target attributes are available to the upper level
> >> >> join . As for semi-join itself, all conditions are still deparsed on
> >> >> left/right join boundary, they are just not propagated further.
> >> >> This shouldn't be a problem, as they are evaluated in subquery. As for
> >> >> left/right join without semi-join beneath it - its behavior is not
> >> >> affected
> >> >> (as hidden_subquery_rels is empty).
> >> >
> >> > Thank you for the explanation. But I have another question. Aren't
> >> > the checks you've proposed too strict? hidden_subquery_rels are
> >> > propagated all the way to the join tree. So, pulling conditions would
> >> > be disables all the way to the join tree too. Is it enough to just
> >> > disable pulling conditions directly from semi-joins, then their
> >> > further pulls will be disabled automatically? See the attached patch.
> >> > It also contains other (mostly cosmetic improvements).
> >> >
> >> > ------
> >> > Regards,
> >> > Alexander Korotkov
> >> > Supabase
> >>
> >> Hi. No, they are not too strict. Look at the following example
> >>
> >> EXPLAIN (verbose, costs off)
> >> SELECT x1.c1 FROM
> >> (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4
> >> WHERE
> >> ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x1
> >> RIGHT JOIN
> >> (SELECT * FROM ft2 WHERE EXISTS (SELECT 1 FROM ft4
> >> WHERE
> >> ft4.c1 = ft2.c1 AND ft2.c2 < 10)) x2
> >> ON (x1.c1 = x2.c1)
> >> LEFT JOIN
> >> (SELECT * FROM ft2 WHERE c2 < 11) x3
> >> ON (x1.c1 = x3.c1)
> >> ORDER BY x1.c1 LIMIT 10;
> >>
> >> With patch which you suggest, we'll deparse left part of left join as
> >> subquery, but will try to pop c2 < 10 condition from
> >> (8) LEFT JOIN ((6) SEMI JOIN (7)) subquery. When we look at left join
> >> of
> >> this subquery and ft2, we still deparse left part as
> >> subquery, so can't pop up conditions from it.
> >
> > I've checked, this query seems to result in the exactly same remote
> > SQLs with your and mine patches. Could you elaborate more on the
> > difference? Do you think foreign_join_ok() can give different results
> > on this query?
>
> Hi.
> With your patch this example gives the same
> ERROR: unexpected expression in subquery output
>
> This happens, because we don't keep knowledge that we have deparsed all
> semi-joins below this left join. As long as left/right join has
> semi-join in its left or right part, this part will be deparsed as
> subquery (look at the following lines in foreign_join_ok()):
>
> else if (jointype == JOIN_LEFT || jointype == JOIN_RIGHT ||
> jointype == JOIN_FULL)
> {
> /*
> * Conditions, generated from semi-joins, should be
> evaluated before
> * LEFT/RIGHT/FULL join.
> */
> if (!bms_is_empty(fpinfo_o->hidden_subquery_rels))
> {
> fpinfo->make_outerrel_subquery = true;
> fpinfo->lower_subquery_rels =
> bms_add_members(fpinfo->lower_subquery_rels, outerrel->relids);
> }
>
> if (!bms_is_empty(fpinfo_i->hidden_subquery_rels))
> {
> fpinfo->make_innerrel_subquery = true;
> fpinfo->lower_subquery_rels =
> bms_add_members(fpinfo->lower_subquery_rels, innerrel->relids);
> }
> }
>
>
> So, we still can't refer to its remote_conds from upper level queries
> (as not all Vars are available from subquery after subquery is created
> in one part of left or right join). It's not necessary to have semi-join
> for this as immediate left/right join inner or outer for inner/outer to
> be deparsed as subquery. But it shouldn't be an issue - we've already
> used remote_conds when created this subquery.
> What I'm trying to say - logic of 'making subquery' and extracting
> conditions should match (or we need more sophisticated way of forming
> subquery targetlist, so that extracted conditions could be used above
> subqueries).
Thank you for the explanation. Pushed. However, it would be nice in
future to rework this in a way that semi-joins on lower levels of join
tree don't have negative impact on optimization of upper levels of
join tree.
------
Regards,
Alexander Korotkov
Supabase
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v56 1/3] Allow logical replication snapshots to be database-specific
@ 2026-04-06 22:07 Álvaro Herrera <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Álvaro Herrera @ 2026-04-06 22:07 UTC (permalink / raw)
By default, the logical decoding assumes access to shared catalogs, so
the snapshot builder needs to consider cluster-wide XIDs during startup.
That in turn means that, if any transaction is already running (and has
XID assigned), the snapshot builder needs to wait for its completion, as
it does not know if that transaction performed catalog changes earlier.
A possible problem with this concept is that if REPACK (CONCURRENTLY) is
running in some database, backends running the same command in other
databases get stuck until the first one has committed. Thus only a
single backend in the cluster can run REPACK (CONCURRENTLY) at any time.
Likewise, REPACK (CONCURRENTLY) can block walsenders starting on behalf
of subscriptions throughout the cluster.
This patch adds a new option to logical replication output plugin, to
declare that it does not use shared catalogs (i.e. catalogs that can be
changed by transactions running in other databases in the cluster). In
that case, no snapshot the backend will use during the decoding needs to
contain information about transactions running in other databases. Thus
the snapshot builder only needs to wait for completion of transactions
in the current database.
Currently we only use this option in the REPACK background worker. It
could possibly be used in the plugin for logical replication too,
however that would need thorough analysis of that plugin.
Bump WAL version number, due to a new field in xl_running_xacts.
Author: Antonin Houska <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Discussion: https://postgr.es/m/90475.1775218118@localhost
---
contrib/pg_visibility/pg_visibility.c | 4 +-
doc/src/sgml/logicaldecoding.sgml | 4 ++
src/backend/access/index/genam.c | 8 +++
src/backend/access/rmgrdesc/standbydesc.c | 2 +
src/backend/access/transam/xlog.c | 2 +-
src/backend/access/transam/xlogfuncs.c | 2 +-
src/backend/postmaster/bgwriter.c | 2 +-
src/backend/replication/logical/decode.c | 17 +++++-
src/backend/replication/logical/logical.c | 3 +
src/backend/replication/logical/snapbuild.c | 63 ++++++++++++++++++++-
src/backend/replication/pgrepack/pgrepack.c | 7 +++
src/backend/replication/slot.c | 2 +-
src/backend/storage/ipc/procarray.c | 23 +++++++-
src/backend/storage/ipc/standby.c | 24 +++++++-
src/include/access/genam.h | 1 +
src/include/access/xlog_internal.h | 2 +-
src/include/miscadmin.h | 9 +++
src/include/replication/output_plugin.h | 1 +
src/include/replication/snapbuild.h | 3 +-
src/include/storage/procarray.h | 2 +-
src/include/storage/standby.h | 3 +-
src/include/storage/standbydefs.h | 1 +
22 files changed, 167 insertions(+), 18 deletions(-)
diff --git a/contrib/pg_visibility/pg_visibility.c b/contrib/pg_visibility/pg_visibility.c
index dfab0b64cf5..d564bd2a00c 100644
--- a/contrib/pg_visibility/pg_visibility.c
+++ b/contrib/pg_visibility/pg_visibility.c
@@ -621,7 +621,7 @@ GetStrictOldestNonRemovableTransactionId(Relation rel)
else if (rel == NULL || rel->rd_rel->relisshared)
{
/* Shared relation: take into account all running xids */
- runningTransactions = GetRunningTransactionData();
+ runningTransactions = GetRunningTransactionData(InvalidOid);
LWLockRelease(ProcArrayLock);
LWLockRelease(XidGenLock);
return runningTransactions->oldestRunningXid;
@@ -632,7 +632,7 @@ GetStrictOldestNonRemovableTransactionId(Relation rel)
* Normal relation: take into account xids running within the current
* database
*/
- runningTransactions = GetRunningTransactionData();
+ runningTransactions = GetRunningTransactionData(InvalidOid);
LWLockRelease(ProcArrayLock);
LWLockRelease(XidGenLock);
return runningTransactions->oldestDatabaseRunningXid;
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index 6dc49108997..9b1d68d0de6 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -959,6 +959,7 @@ typedef struct OutputPluginOptions
{
OutputPluginOutputType output_type;
bool receive_rewrites;
+ bool need_shared_catalogs;
} OutputPluginOptions;
</programlisting>
<literal>output_type</literal> has to either be set to
@@ -969,6 +970,9 @@ typedef struct OutputPluginOptions
also be called for changes made by heap rewrites during certain DDL
operations. These are of interest to plugins that handle DDL
replication, but they require special handling.
+ <literal>need_shared_catalogs</literal> can be set to false if you are
+ certain the plugin functions do not access shared system catalogs.
+ Doing so can speed up creation of replication slots that use this plugin.
</para>
<para>
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 1408989c568..97d44b84622 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -394,6 +394,14 @@ systable_beginscan(Relation heapRelation,
SysScanDesc sysscan;
Relation irel;
+ /*
+ * If this backend promised that it won't access shared catalogs during
+ * logical decoding, this it the right place to verify.
+ */
+ Assert(!HistoricSnapshotActive() ||
+ accessSharedCatalogsInDecoding ||
+ !heapRelation->rd_rel->relisshared);
+
if (indexOK &&
!IgnoreSystemIndexes &&
!ReindexIsProcessingIndex(indexId))
diff --git a/src/backend/access/rmgrdesc/standbydesc.c b/src/backend/access/rmgrdesc/standbydesc.c
index 0a291354ae2..685d1bdb024 100644
--- a/src/backend/access/rmgrdesc/standbydesc.c
+++ b/src/backend/access/rmgrdesc/standbydesc.c
@@ -41,6 +41,8 @@ standby_desc_running_xacts(StringInfo buf, xl_running_xacts *xlrec)
for (i = 0; i < xlrec->subxcnt; i++)
appendStringInfo(buf, " %u", xlrec->xids[xlrec->xcnt + i]);
}
+
+ appendStringInfo(buf, "; dbid: %u", xlrec->dbid);
}
void
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b82af9a85c0..3f08a832ca6 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7735,7 +7735,7 @@ CreateCheckPoint(int flags)
* recovery we don't need to write running xact data.
*/
if (!shutdown && XLogStandbyInfoActive())
- LogStandbySnapshot();
+ LogStandbySnapshot(InvalidOid);
START_CRIT_SECTION();
diff --git a/src/backend/access/transam/xlogfuncs.c b/src/backend/access/transam/xlogfuncs.c
index 65bbaeda59c..0f5979691e6 100644
--- a/src/backend/access/transam/xlogfuncs.c
+++ b/src/backend/access/transam/xlogfuncs.c
@@ -245,7 +245,7 @@ pg_log_standby_snapshot(PG_FUNCTION_ARGS)
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("pg_log_standby_snapshot() can only be used if \"wal_level\" >= \"replica\"")));
- recptr = LogStandbySnapshot();
+ recptr = LogStandbySnapshot(InvalidOid);
/*
* As a convenience, return the WAL location of the last inserted record
diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c
index 1d8947774a9..a30de4262eb 100644
--- a/src/backend/postmaster/bgwriter.c
+++ b/src/backend/postmaster/bgwriter.c
@@ -289,7 +289,7 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len)
if (now >= timeout &&
last_snapshot_lsn <= GetLastImportantRecPtr())
{
- last_snapshot_lsn = LogStandbySnapshot();
+ last_snapshot_lsn = LogStandbySnapshot(InvalidOid);
last_snapshot_ts = now;
}
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index c9fea8cad28..38c5a4f5540 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -382,7 +382,16 @@ standby_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
{
xl_running_xacts *running = (xl_running_xacts *) XLogRecGetData(r);
- SnapBuildProcessRunningXacts(builder, buf->origptr, running);
+ /*
+ * Update this decoder's idea of transactions currently
+ * running. In doing so we will determine whether we have
+ * reached consistent status.
+ *
+ * If the output plugin doesn't need access to shared
+ * catalogs, we can ignore transactions in other databases.
+ */
+ SnapBuildProcessRunningXacts(builder, buf->origptr, running,
+ !ctx->options.need_shared_catalogs);
/*
* Abort all transactions that we keep track of, that are
@@ -392,8 +401,12 @@ standby_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
* all running transactions which includes prepared ones,
* while shutdown checkpoints just know that no non-prepared
* transactions are in progress.
+ *
+ * The database-specific records might work here too, but it's
+ * not their purpose.
*/
- ReorderBufferAbortOld(ctx->reorder, running->oldestRunningXid);
+ if (!OidIsValid(running->dbid))
+ ReorderBufferAbortOld(ctx->reorder, running->oldestRunningXid);
}
break;
case XLOG_STANDBY_LOCK:
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index f20d0c542f3..8ceaf64d164 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -285,6 +285,9 @@ StartupDecodingContext(List *output_plugin_options,
ctx->write = do_write;
ctx->update_progress = update_progress;
+ /* Assume shared catalog access. The startup callback can change it. */
+ ctx->options.need_shared_catalogs = true;
+
ctx->output_plugin_options = output_plugin_options;
ctx->fast_forward = fast_forward;
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index b4269a3b102..ffb4ab2cf2a 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -154,6 +154,14 @@
static ResourceOwner SavedResourceOwnerDuringExport = NULL;
static bool ExportInProgress = false;
+/*
+ * If a backend is going to do logical decoding and the output plugin does
+ * not need to access shared catalogs, setting this variable to false can make
+ * the decoding startup faster. In particular, the backend will not need to
+ * wait for completion of already running transactions in other databases.
+ */
+bool accessSharedCatalogsInDecoding = true;
+
/* ->committed and ->catchange manipulation */
static void SnapBuildPurgeOlderTxn(SnapBuild *builder);
@@ -170,7 +178,8 @@ static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, Transaction
uint32 xinfo);
/* xlog reading helper functions for SnapBuildProcessRunningXacts */
-static bool SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *running);
+static bool SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn,
+ xl_running_xacts *running);
static void SnapBuildWaitSnapshot(xl_running_xacts *running, TransactionId cutoff);
/* serialization functions */
@@ -226,6 +235,9 @@ AllocateSnapshotBuilder(ReorderBuffer *reorder,
MemoryContextSwitchTo(oldcontext);
+ /* The default is that shared catalog are used. */
+ accessSharedCatalogsInDecoding = true;
+
return builder;
}
@@ -244,6 +256,9 @@ FreeSnapshotBuilder(SnapBuild *builder)
builder->snapshot = NULL;
}
+ /* The default is that shared catalog are used. */
+ accessSharedCatalogsInDecoding = true;
+
/* other resources are deallocated via memory context reset */
MemoryContextDelete(context);
}
@@ -1136,7 +1151,8 @@ SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
* anymore.
*/
void
-SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *running)
+SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *running,
+ bool db_specific)
{
ReorderBufferTXN *txn;
TransactionId xmin;
@@ -1148,6 +1164,33 @@ SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xact
*/
if (builder->state < SNAPBUILD_CONSISTENT)
{
+ /*
+ * To reduce the potential for unnecessarily waiting for completion of
+ * unrelated transactions, the caller can declare that only
+ * transactions of the current database are relevant at this stage.
+ */
+ if (db_specific)
+ {
+ /*
+ * If we must only keep track of transactions running in the
+ * current database, we need transaction info from exactly that
+ * database.
+ */
+ if (running->dbid != MyDatabaseId)
+ {
+ LogStandbySnapshot(MyDatabaseId);
+
+ return;
+ }
+
+ /*
+ * We'd better be able to check during scan if the plugin does not
+ * lie.
+ */
+ if (accessSharedCatalogsInDecoding)
+ accessSharedCatalogsInDecoding = false;
+ }
+
/* returns false if there's no point in performing cleanup just yet */
if (!SnapBuildFindSnapshot(builder, lsn, running))
return;
@@ -1155,6 +1198,16 @@ SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xact
else
SnapBuildSerialize(builder, lsn);
+ /*
+ * Database specific transaction info may exist to reach CONSISTENT state
+ * faster, however the code below makes no use of it. Moreover, such
+ * record might cause problems because the following normal (cluster-wide)
+ * record can have lower value of oldestRunningXid. In that case, let's
+ * wait with the cleanup for the next regular cluster-wide record.
+ */
+ if (OidIsValid(running->dbid))
+ return;
+
/*
* Update range of interesting xids based on the running xacts
* information. We don't increase ->xmax using it, because once we are in
@@ -1465,7 +1518,11 @@ SnapBuildWaitSnapshot(xl_running_xacts *running, TransactionId cutoff)
*/
if (!RecoveryInProgress())
{
- LogStandbySnapshot();
+ /*
+ * If the last transaction info was about specific database, so needs
+ * to be the next one - at least until we're in the CONSISTENT state.
+ */
+ LogStandbySnapshot(running->dbid);
}
}
diff --git a/src/backend/replication/pgrepack/pgrepack.c b/src/backend/replication/pgrepack/pgrepack.c
index 457b6518a8e..4da66aa0f44 100644
--- a/src/backend/replication/pgrepack/pgrepack.c
+++ b/src/backend/replication/pgrepack/pgrepack.c
@@ -52,6 +52,13 @@ repack_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt,
/* Probably unnecessary, as we don't use the SQL interface ... */
opt->output_type = OUTPUT_PLUGIN_BINARY_OUTPUT;
+ /*
+ * REPACK doesn't need access to shared catalogs, so we can speed up the
+ * historic snapshot creation by setting this flag. We'll only have to
+ * wait for transactions in our database.
+ */
+ opt->need_shared_catalogs = false;
+
if (ctx->output_plugin_options != NIL)
{
ereport(ERROR,
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 21a213a0ebf..a1f37e59dbc 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -1756,7 +1756,7 @@ ReplicationSlotReserveWal(void)
XLogRecPtr flushptr;
/* make sure we have enough information to start */
- flushptr = LogStandbySnapshot();
+ flushptr = LogStandbySnapshot(InvalidOid);
/* and make sure it's fsynced to disk */
XLogFlush(flushptr);
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index f540bb6b23f..9299bcebbda 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -2623,9 +2623,11 @@ ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc)
*
* Note that if any transaction has overflowed its cached subtransactions
* then there is no real need include any subtransactions.
+ *
+ * If 'dbid' is valid, only gather transactions running in that database.
*/
RunningTransactions
-GetRunningTransactionData(void)
+GetRunningTransactionData(Oid dbid)
{
/* result workspace */
static RunningTransactionsData CurrentRunningXactsData;
@@ -2700,6 +2702,18 @@ GetRunningTransactionData(void)
if (!TransactionIdIsValid(xid))
continue;
+ /*
+ * Filter by database OID if requested.
+ */
+ if (OidIsValid(dbid))
+ {
+ int pgprocno = arrayP->pgprocnos[index];
+ PGPROC *proc = &allProcs[pgprocno];
+
+ if (proc->databaseId != dbid)
+ continue;
+ }
+
/*
* Be careful not to exclude any xids before calculating the values of
* oldestRunningXid and suboverflowed, since these are used to clean
@@ -2750,6 +2764,12 @@ GetRunningTransactionData(void)
PGPROC *proc = &allProcs[pgprocno];
int nsubxids;
+ /*
+ * Filter by database OID if requested.
+ */
+ if (OidIsValid(dbid) && proc->databaseId != dbid)
+ continue;
+
/*
* Save subtransaction XIDs. Other backends can't add or remove
* entries while we're holding XidGenLock.
@@ -2783,6 +2803,7 @@ GetRunningTransactionData(void)
* increases if slots do.
*/
+ CurrentRunningXacts->dbid = dbid;
CurrentRunningXacts->xcnt = count - subcount;
CurrentRunningXacts->subxcnt = subcount;
CurrentRunningXacts->subxid_status = suboverflowed ? SUBXIDS_IN_SUBTRANS : SUBXIDS_IN_ARRAY;
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index de9092fdf5b..29af7733948 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1188,6 +1188,14 @@ standby_redo(XLogReaderState *record)
xl_running_xacts *xlrec = (xl_running_xacts *) XLogRecGetData(record);
RunningTransactionsData running;
+ /*
+ * Records issued for specific database are not suitable for physical
+ * replication because that affects the whole cluster. In particular,
+ * the list of XID is probably incomplete here.
+ */
+ if (OidIsValid(xlrec->dbid))
+ return;
+
running.xcnt = xlrec->xcnt;
running.subxcnt = xlrec->subxcnt;
running.subxid_status = xlrec->subxid_overflow ? SUBXIDS_MISSING : SUBXIDS_IN_ARRAY;
@@ -1277,11 +1285,22 @@ standby_redo(XLogReaderState *record)
* as there's no independent knob to just enable logical decoding. For
* details of how this is used, check snapbuild.c's introductory comment.
*
+ * If 'dbid' is valid, only gather transactions running in that
+ * database. snapbuild.c can use such running xacts information for faster
+ * startup, but it still needs normal (cluster-wide) during the actual
+ * decoding - see standby_decode() and SnapBuildProcessRunningXacts() for
+ * details. Other processes (e.g. checkpointer) issue the cluster-wide records
+ * whether logical decoding is active or not.
+ *
+ * Please be careful about using this argument for other purposes. In
+ * particular, physical replication *must* ignore the database-specific
+ * records, exactly because they do not cover the whole cluster - see
+ * standby_redo().
*
* Returns the RecPtr of the last inserted record.
*/
XLogRecPtr
-LogStandbySnapshot(void)
+LogStandbySnapshot(Oid dbid)
{
XLogRecPtr recptr;
RunningTransactions running;
@@ -1314,7 +1333,7 @@ LogStandbySnapshot(void)
* Log details of all in-progress transactions. This should be the last
* record we write, because standby will open up when it sees this.
*/
- running = GetRunningTransactionData();
+ running = GetRunningTransactionData(dbid);
/*
* GetRunningTransactionData() acquired ProcArrayLock, we must release it.
@@ -1358,6 +1377,7 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts)
xl_running_xacts xlrec;
XLogRecPtr recptr;
+ xlrec.dbid = CurrRunningXacts->dbid;
xlrec.xcnt = CurrRunningXacts->xcnt;
xlrec.subxcnt = CurrRunningXacts->subxcnt;
xlrec.subxid_overflow = (CurrRunningXacts->subxid_status != SUBXIDS_IN_ARRAY);
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index b69320a7fc8..de55821e414 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -136,6 +136,7 @@ typedef struct IndexOrderByDistance
bool isnull;
} IndexOrderByDistance;
+
/*
* generalized index_ interface routines (in indexam.c)
*/
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 10c18d39ff8..13ae3ad4fbb 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -32,7 +32,7 @@
/*
* Each page of XLOG file has a header like this:
*/
-#define XLOG_PAGE_MAGIC 0xD11E /* can be used as WAL version indicator */
+#define XLOG_PAGE_MAGIC 0xD11F /* can be used as WAL version indicator */
typedef struct XLogPageHeaderData
{
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 2e10e3c814d..93b7816c09c 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -310,6 +310,15 @@ extern void PreventCommandIfReadOnly(const char *cmdname);
extern void PreventCommandIfParallelMode(const char *cmdname);
extern void PreventCommandDuringRecovery(const char *cmdname);
+/* in replication/snapbuild.c */
+
+/*
+ * Keep track of whether logical decoding in this backend promised not to
+ * access shared catalogs, as a safety check. This is checked by genam.c when
+ * a catalog scan takes place to verify that no shared catalogs are accessed.
+ */
+extern bool accessSharedCatalogsInDecoding;
+
/*****************************************************************************
* pdir.h -- *
* POSTGRES directory path definitions. *
diff --git a/src/include/replication/output_plugin.h b/src/include/replication/output_plugin.h
index 842fcde67f9..917f3cff232 100644
--- a/src/include/replication/output_plugin.h
+++ b/src/include/replication/output_plugin.h
@@ -27,6 +27,7 @@ typedef struct OutputPluginOptions
{
OutputPluginOutputType output_type;
bool receive_rewrites;
+ bool need_shared_catalogs;
} OutputPluginOptions;
/*
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index a22a83a2f23..d02530a912a 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -92,7 +92,8 @@ extern void SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
XLogRecPtr lsn,
xl_heap_new_cid *xlrec);
extern void SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn,
- xl_running_xacts *running);
+ xl_running_xacts *running,
+ bool db_specific);
extern void SnapBuildSerializationPoint(SnapBuild *builder, XLogRecPtr lsn);
extern bool SnapBuildSnapshotExists(XLogRecPtr lsn);
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index d718a5b542f..ec89c448220 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -47,7 +47,7 @@ extern bool ProcArrayInstallImportedXmin(TransactionId xmin,
VirtualTransactionId *sourcevxid);
extern bool ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc);
-extern RunningTransactions GetRunningTransactionData(void);
+extern RunningTransactions GetRunningTransactionData(Oid dbid);
extern bool TransactionIdIsInProgress(TransactionId xid);
extern TransactionId GetOldestNonRemovableTransactionId(Relation rel);
diff --git a/src/include/storage/standby.h b/src/include/storage/standby.h
index 6a314c693cd..8715c08e94f 100644
--- a/src/include/storage/standby.h
+++ b/src/include/storage/standby.h
@@ -126,6 +126,7 @@ typedef enum
typedef struct RunningTransactionsData
{
+ Oid dbid; /* only track xacts in this database */
int xcnt; /* # of xact ids in xids[] */
int subxcnt; /* # of subxact ids in xids[] */
subxids_array_status subxid_status;
@@ -143,7 +144,7 @@ typedef RunningTransactionsData *RunningTransactions;
extern void LogAccessExclusiveLock(Oid dbOid, Oid relOid);
extern void LogAccessExclusiveLockPrepare(void);
-extern XLogRecPtr LogStandbySnapshot(void);
+extern XLogRecPtr LogStandbySnapshot(Oid dbid);
extern void LogStandbyInvalidations(int nmsgs, SharedInvalidationMessage *msgs,
bool relcacheInitFileInval);
diff --git a/src/include/storage/standbydefs.h b/src/include/storage/standbydefs.h
index 231d251fd51..e75b7078766 100644
--- a/src/include/storage/standbydefs.h
+++ b/src/include/storage/standbydefs.h
@@ -46,6 +46,7 @@ typedef struct xl_standby_locks
*/
typedef struct xl_running_xacts
{
+ Oid dbid; /* only track xacts in this database */
int xcnt; /* # of xact ids in xids[] */
int subxcnt; /* # of subxact ids in xids[] */
bool subxid_overflow; /* snapshot overflowed, subxids missing */
--
2.47.3
--qs77q6fpwolne4ds
Content-Type: text/x-diff; charset=utf-8
Content-Disposition: attachment;
filename="v56-0002-Reserve-replication-slots-specifically-for-REPAC.patch"
^ permalink raw reply [nested|flat] 19+ messages in thread
end of thread, other threads:[~2026-04-06 22:07 UTC | newest]
Thread overview: 19+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2023-01-19 17:49 Re: Add semi-join pushdown to postgres_fdw Tomas Vondra <[email protected]>
2023-01-20 09:00 ` Alexander Pyhalov <[email protected]>
2023-10-30 16:05 ` Alexander Korotkov <[email protected]>
2023-10-31 11:07 ` Alexander Pyhalov <[email protected]>
2023-11-27 00:49 ` Alexander Korotkov <[email protected]>
2023-11-27 15:11 ` Alexander Pyhalov <[email protected]>
2023-12-03 20:52 ` Alexander Korotkov <[email protected]>
2025-03-18 00:20 ` Robins Tharakan <[email protected]>
2025-03-18 00:27 ` Alexander Korotkov <[email protected]>
2025-03-18 11:13 ` Alexander Pyhalov <[email protected]>
2025-03-18 11:19 ` Alexander Korotkov <[email protected]>
2025-03-18 16:04 ` Alexander Pyhalov <[email protected]>
2025-03-24 01:21 ` Alexander Korotkov <[email protected]>
2025-03-24 07:07 ` Alexander Pyhalov <[email protected]>
2025-03-24 08:49 ` Alexander Korotkov <[email protected]>
2025-03-24 16:56 ` Alexander Pyhalov <[email protected]>
2025-03-25 07:04 ` Tender Wang <[email protected]>
2025-03-25 10:55 ` Alexander Korotkov <[email protected]>
2026-04-06 22:07 [PATCH v56 1/3] Allow logical replication snapshots to be database-specific Álvaro Herrera <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox