From b5908a7ac4ea6f40493cb0d5638d6a254a4fb768 Mon Sep 17 00:00:00 2001 From: Dinesh Salve Date: Wed, 5 Nov 2025 05:49:11 +0000 Subject: [PATCH 1/1] This change adds capability to fetch explain plans for foreign tables. We have introduced new option "remote_plans" to achieve the same. This option does not work with ANALYZE option yet. --- .../postgres_fdw/expected/postgres_fdw.out | 581 ++++++++++++++++++ contrib/postgres_fdw/option.c | 71 +++ contrib/postgres_fdw/postgres_fdw.c | 256 +++++++- contrib/postgres_fdw/postgres_fdw.h | 26 + contrib/postgres_fdw/sql/postgres_fdw.sql | 39 ++ 5 files changed, 964 insertions(+), 9 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index cd28126049d..5496a6ddea5 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -441,6 +441,173 @@ SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1; fixed | (1 row) +-- with WHERE clause and remote_plans with different formats +EXPLAIN (REMOTE_PLANS, FORMAT YAML, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; + QUERY PLAN +----------------------------------------------------------------------------------------------------------- + - Plan: + + Node Type: "Foreign Scan" + + Operation: "Select" + + Parallel Aware: false + + Async Capable: false + + Relation Name: "ft1" + + Schema: "public" + + Alias: "t1" + + Disabled: false + + Output: + + - "c1" + + - "c2" + + - "c3" + + - "c4" + + - "c5" + + - "c6" + + - "c7" + + - "c8" + + Remote SQL: "SELECT \"C 1\", c2, c3, c4, c5, c6, c7, c8 FROM \"S 1\".\"T 1\" WHERE ((\"C 1\" = 101))"+ + Plan Node ID: 0 + + Remote Plans: + + Plan Node ID 0: + + - Plan: + + Node Type: "Index Scan" + + Parallel Aware: false + + Async Capable: false + + Scan Direction: "Forward" + + Index Name: "t1_pkey" + + Relation Name: "T 1" + + Schema: "S 1" + + Alias: "T 1" + + Disabled: false + + Output: + + - "\"C 1\"" + + - "c2" + + - "c3" + + - "c4" + + - "c5" + + - "c6" + + - "c7" + + - "c8" + + Index Cond: "(\"T 1\".\"C 1\" = 101)" +(1 row) + +EXPLAIN (REMOTE_PLANS TRUE, FORMAT XML, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------- + + + + + + + Foreign Scan + + Select + + false + + false + + ft1 + + public + + t1 + + false + + + + c1 + + c2 + + c3 + + c4 + + c5 + + c6 + + c7 + + c8 + + + + SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 101))+ + 0 + + + + + + + + + + + + + + Index Scan + + false + + false + + Forward + + t1_pkey + + T 1 + + S 1 + + T 1 + + false + + + + "C 1" + + c2 + + c3 + + c4 + + c5 + + c6 + + c7 + + c8 + + + + ("T 1"."C 1" = 101) + + + + + + + + + + + + + + +(1 row) + +EXPLAIN (REMOTE_PLANS, FORMAT JSON, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------- + [ + + { + + "Plan": { + + "Node Type": "Foreign Scan", + + "Operation": "Select", + + "Parallel Aware": false, + + "Async Capable": false, + + "Relation Name": "ft1", + + "Schema": "public", + + "Alias": "t1", + + "Disabled": false, + + "Output": ["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8"], + + "Remote SQL": "SELECT \"C 1\", c2, c3, c4, c5, c6, c7, c8 FROM \"S 1\".\"T 1\" WHERE ((\"C 1\" = 101))",+ + "Plan Node ID": 0 + + }, + + "Remote Plans": { + + "Plan Node ID 0": [ + + [ + + { + + "Plan": { + + "Node Type": "Index Scan", + + "Parallel Aware": false, + + "Async Capable": false, + + "Scan Direction": "Forward", + + "Index Name": "t1_pkey", + + "Relation Name": "T 1", + + "Schema": "S 1", + + "Alias": "T 1", + + "Disabled": false, + + "Output": ["\"C 1\"", "c2", "c3", "c4", "c5", "c6", "c7", "c8"], + + "Index Cond": "(\"T 1\".\"C 1\" = 101)" + + } + + } + + ] + + ] + + } + + } + + ] +(1 row) + +EXPLAIN (REMOTE_PLANS TRUE, FORMAT TEXT, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Foreign Scan on public.ft1 t1 + Output: c1, c2, c3, c4, c5, c6, c7, c8 + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 101)) + Plan Node ID: 0 + Remote Plans: + ------------- + Plan Node ID 0: + Index Scan using t1_pkey on "S 1"."T 1" + Output: "C 1", c2, c3, c4, c5, c6, c7, c8 + Index Cond: ("T 1"."C 1" = 101) +(10 rows) + -- Test forcing the remote server to produce sorted data for a merge join. SET enable_hashjoin TO false; SET enable_nestloop TO false; @@ -5086,6 +5253,373 @@ SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE 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) +-- EXPLAIN remote_plans +EXPLAIN (remote_plans, format text, costs off, analyze) +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; +ERROR: EXPLAIN options REMOTE_PLANS and ANALYZE cannot be used together +EXPLAIN (remote_plans, format text, 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 + Plan Node ID: 0 + -> Merge Semi Join + Merge Cond: (ft1.c1 = ft2_1.c1) + Plan Node ID: 1 + -> Foreign Scan + Relations: (ft1) INNER JOIN (ft2) + Plan Node ID: 2 + -> Foreign Scan + Relations: (ft2 ft2_1) INNER JOIN (ft4) + Plan Node ID: 3 + Remote Plans: + ------------- + Plan Node ID 2: + Index Only Scan using t1_pkey on "T 1" r2 + Plan Node ID 3: + Merge Join + Merge Cond: (r5."C 1" = r6.c1) + -> Index Only Scan using t1_pkey on "T 1" r5 + -> Sort + Sort Key: r6.c1 + -> Seq Scan on "T 3" r6 +(22 rows) + +EXPLAIN (remote_plans, format xml, 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 + + false + + false + + false + + 0 + + + + + + Merge Join + + Outer + + false + + false + + Semi + + false + + false + + (ft1.c1 = ft2_1.c1) + + 1 + + + + + + Foreign Scan + + Select + + Outer + + false + + false + + false + + (ft1) INNER JOIN (ft2) + + 2 + + + + + + Foreign Scan + + Select + + Inner + + false + + false + + false + + (ft2 ft2_1) INNER JOIN (ft4) + + 3 + + + + + + + + + + + + + + + + + + + + + + Index Only Scan + + false + + false + + Forward + + t1_pkey + + T 1 + + r2 + + false + + + + + + + + + + + + + + + + + + Merge Join + + false + + false + + Inner + + false + + true + + (r5."C 1" = r6.c1) + + + + + + Index Only Scan + + Outer + + false + + false + + Forward + + t1_pkey + + T 1 + + r5 + + false + + + + + + Sort + + Inner + + false + + false + + false + + + + r6.c1 + + + + + + + + Seq Scan + + Outer+ + false + + false + + T 3 + + r6 + + false + + + + + + + + + + + + + + + + + + + + + + +(1 row) + +EXPLAIN (remote_plans, format json, 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 +------------------------------------------------------------ + [ + + { + + "Plan": { + + "Node Type": "Limit", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Plan Node ID": 0, + + "Plans": [ + + { + + "Node Type": "Merge Join", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Join Type": "Semi", + + "Disabled": false, + + "Inner Unique": false, + + "Merge Cond": "(ft1.c1 = ft2_1.c1)", + + "Plan Node ID": 1, + + "Plans": [ + + { + + "Node Type": "Foreign Scan", + + "Operation": "Select", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Relations": "(ft1) INNER JOIN (ft2)", + + "Plan Node ID": 2 + + }, + + { + + "Node Type": "Foreign Scan", + + "Operation": "Select", + + "Parent Relationship": "Inner", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Relations": "(ft2 ft2_1) INNER JOIN (ft4)",+ + "Plan Node ID": 3 + + } + + ] + + } + + ] + + }, + + "Remote Plans": { + + "Plan Node ID 2": [ + + [ + + { + + "Plan": { + + "Node Type": "Index Only Scan", + + "Parallel Aware": false, + + "Async Capable": false, + + "Scan Direction": "Forward", + + "Index Name": "t1_pkey", + + "Relation Name": "T 1", + + "Alias": "r2", + + "Disabled": false + + } + + } + + ] + + ], + + "Plan Node ID 3": [ + + [ + + { + + "Plan": { + + "Node Type": "Merge Join", + + "Parallel Aware": false, + + "Async Capable": false, + + "Join Type": "Inner", + + "Disabled": false, + + "Inner Unique": true, + + "Merge Cond": "(r5.\"C 1\" = r6.c1)", + + "Plans": [ + + { + + "Node Type": "Index Only Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Scan Direction": "Forward", + + "Index Name": "t1_pkey", + + "Relation Name": "T 1", + + "Alias": "r5", + + "Disabled": false + + }, + + { + + "Node Type": "Sort", + + "Parent Relationship": "Inner", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Sort Key": ["r6.c1"], + + "Plans": [ + + { + + "Node Type": "Seq Scan", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Relation Name": "T 3", + + "Alias": "r6", + + "Disabled": false + + } + + ] + + } + + ] + + } + + } + + ] + + ] + + } + + } + + ] +(1 row) + +EXPLAIN (remote_plans, format yaml, 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 +------------------------------------------------------- + - Plan: + + Node Type: "Limit" + + Parallel Aware: false + + Async Capable: false + + Disabled: false + + Plan Node ID: 0 + + Plans: + + - Node Type: "Merge Join" + + Parent Relationship: "Outer" + + Parallel Aware: false + + Async Capable: false + + Join Type: "Semi" + + Disabled: false + + Inner Unique: false + + Merge Cond: "(ft1.c1 = ft2_1.c1)" + + Plan Node ID: 1 + + Plans: + + - Node Type: "Foreign Scan" + + Operation: "Select" + + Parent Relationship: "Outer" + + Parallel Aware: false + + Async Capable: false + + Disabled: false + + Relations: "(ft1) INNER JOIN (ft2)" + + Plan Node ID: 2 + + - Node Type: "Foreign Scan" + + Operation: "Select" + + Parent Relationship: "Inner" + + Parallel Aware: false + + Async Capable: false + + Disabled: false + + Relations: "(ft2 ft2_1) INNER JOIN (ft4)"+ + Plan Node ID: 3 + + Remote Plans: + + Plan Node ID 2: + + - Plan: + + Node Type: "Index Only Scan" + + Parallel Aware: false + + Async Capable: false + + Scan Direction: "Forward" + + Index Name: "t1_pkey" + + Relation Name: "T 1" + + Alias: "r2" + + Disabled: false + + Plan Node ID 3: + + - Plan: + + Node Type: "Merge Join" + + Parallel Aware: false + + Async Capable: false + + Join Type: "Inner" + + Disabled: false + + Inner Unique: true + + Merge Cond: "(r5.\"C 1\" = r6.c1)" + + Plans: + + - Node Type: "Index Only Scan" + + Parent Relationship: "Outer" + + Parallel Aware: false + + Async Capable: false + + Scan Direction: "Forward" + + Index Name: "t1_pkey" + + Relation Name: "T 1" + + Alias: "r5" + + Disabled: false + + - Node Type: "Sort" + + Parent Relationship: "Inner" + + Parallel Aware: false + + Async Capable: false + + Disabled: false + + Sort Key: + + - "r6.c1" + + Plans: + + - Node Type: "Seq Scan" + + Parent Relationship: "Outer" + + Parallel Aware: false + + Async Capable: false + + Relation Name: "T 3" + + Alias: "r6" + + Disabled: false +(1 row) + -- =================================================================== -- test writable foreign table stuff -- =================================================================== @@ -6303,6 +6837,25 @@ DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass; ft2 (1 row) +-- test write on foreign tables with remote_plans +EXPLAIN (remote_plans, verbose, costs off) +UPDATE ft2 SET c2 = c2 + 300 WHERE c1 % 10 = 3; + QUERY PLAN +--------------------------------------------------------------------------------------- + Update on public.ft2 + Plan Node ID: 0 + -> Foreign Update on public.ft2 + Remote SQL: UPDATE "S 1"."T 1" SET c2 = (c2 + 300) WHERE ((("C 1" % 10) = 3)) + Plan Node ID: 1 + Remote Plans: + ------------- + Plan Node ID 1: + Update on "S 1"."T 1" + -> Seq Scan on "S 1"."T 1" + Output: (c2 + 300), ctid + Filter: (("T 1"."C 1" % 10) = 3) +(12 rows) + -- Test UPDATE/DELETE with RETURNING on a three-table join INSERT INTO ft2 (c1,c2,c3) SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id; @@ -12260,6 +12813,34 @@ SELECT * FROM insert_tbl ORDER BY a; 2505 | 505 | bar (2 rows) +EXPLAIN (REMOTE_PLANS, VERBOSE, COSTS OFF) +INSERT INTO insert_tbl (SELECT * FROM local_tbl UNION ALL SELECT * FROM remote_tbl); + QUERY PLAN +------------------------------------------------------------------------- + Insert on public.insert_tbl + Remote SQL: INSERT INTO public.base_tbl4(a, b, c) VALUES ($1, $2, $3) + Batch Size: 1 + Plan Node ID: 0 + -> Append + Plan Node ID: 1 + -> Seq Scan on public.local_tbl + Output: local_tbl.a, local_tbl.b, local_tbl.c + Plan Node ID: 2 + -> Async Foreign Scan on public.remote_tbl + Output: remote_tbl.a, remote_tbl.b, remote_tbl.c + Remote SQL: SELECT a, b, c FROM public.base_tbl3 + Plan Node ID: 3 + Remote Plans: + ------------- + Plan Node ID 0: + Insert on public.base_tbl4 + -> Result + Output: $1, $2, $3 + Plan Node ID 3: + Seq Scan on public.base_tbl3 + Output: a, b, c +(22 rows) + -- Check with direct modify EXPLAIN (VERBOSE, COSTS OFF) WITH t AS (UPDATE remote_tbl SET c = c || c RETURNING *) diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c index 04788b7e8b3..a3b87d2bbaf 100644 --- a/contrib/postgres_fdw/option.c +++ b/contrib/postgres_fdw/option.c @@ -17,6 +17,8 @@ #include "catalog/pg_foreign_table.h" #include "catalog/pg_user_mapping.h" #include "commands/defrem.h" +#include "commands/explain.h" +#include "commands/explain_state.h" #include "commands/extension.h" #include "libpq/libpq-be.h" #include "postgres_fdw.h" @@ -40,6 +42,13 @@ typedef struct PgFdwOption */ static PgFdwOption *postgres_fdw_options; +/* + * EXPLAIN hooks + */ +static explain_per_node_hook_type prev_explain_per_node_hook; +static explain_per_plan_hook_type prev_explain_per_plan_hook; +static explain_validate_options_hook_type prev_explain_validate_options_hook; + /* * GUC parameters */ @@ -561,6 +570,57 @@ process_pgfdw_appname(const char *appname) return buf.data; } +/* + * Get the PgFdwExplainState structure from an ExplainState; if there is + * none, create one, attach it to the ExplainState, and return it. + */ +static PgFdwExplainState * +pgfdw_ensure_options(ExplainState *es) +{ + PgFdwExplainState *pgfdw_explain_state; + + pgfdw_explain_state = GetExplainExtensionState(es, GetExplainExtensionId("postgres_fdw")); + + if (pgfdw_explain_state == NULL) + { + pgfdw_explain_state = palloc0(sizeof(PgFdwExplainState)); + SetExplainExtensionState(es, GetExplainExtensionId("postgres_fdw"), pgfdw_explain_state); + pgfdw_explain_state->all_remote_plans = NIL; + } + + return pgfdw_explain_state; +} + +/* + * Parse handler for EXPLAIN (REMOTE_PLANS). + */ +static void +pgfdw_remote_plans_apply(ExplainState *es, DefElem *opt, ParseState *pstate) +{ + PgFdwExplainState *options = pgfdw_ensure_options(es); + + options->remote_plans = defGetBoolean(opt); +} + +static void +postgresExplainValidateOptions(ExplainState *es, List *options, ParseState *pstate) +{ + ListCell *lc; + + foreach(lc, options) + { + DefElem *opt = (DefElem *) lfirst(lc); + + if (strcmp(opt->defname, "remote_plans") == 0) + { + if (defGetBoolean(opt) && es->analyze) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("EXPLAIN options REMOTE_PLANS and ANALYZE cannot be used together")); + } + } +} + /* * Module load callback */ @@ -587,4 +647,15 @@ _PG_init(void) NULL); MarkGUCPrefixReserved("postgres_fdw"); + + RegisterExtensionExplainOption("remote_plans", pgfdw_remote_plans_apply); + + /* per node EXPLAIN hook */ + prev_explain_per_node_hook = explain_per_node_hook; + explain_per_node_hook = postgresExplainPerNode; + prev_explain_per_plan_hook = explain_per_plan_hook; + explain_per_plan_hook = postgresExplainPerPlan; + prev_explain_validate_options_hook = explain_validate_options_hook; + explain_validate_options_hook = postgresExplainValidateOptions; + } diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 06b52c65300..9b99a2386b3 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -43,6 +43,7 @@ #include "utils/builtins.h" #include "utils/float.h" #include "utils/guc.h" +#include "utils/json.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" @@ -133,6 +134,20 @@ enum FdwDirectModifyPrivateIndex FdwDirectModifyPrivateSetProcessed, }; +static const char *const explain_formats[] = { + [EXPLAIN_FORMAT_TEXT] = "TEXT", + [EXPLAIN_FORMAT_JSON] = "JSON", + [EXPLAIN_FORMAT_XML] = "XML", + [EXPLAIN_FORMAT_YAML] = "YAML", +}; + +/* + * Track the extension id in the backend. + */ +static int extension_id = -1; +#define GET_EXTENSION_ID() ((extension_id == -1) ? \ + GetExplainExtensionId("postgres_fdw"): extension_id) + /* * Execution state of a foreign scan using postgres_fdw. */ @@ -2822,6 +2837,65 @@ postgresEndDirectModify(ForeignScanState *node) /* MemoryContext will be deleted automatically. */ } +static void +postgresExplainStatement(int plan_node_id, + ExplainState *es, + PgFdwExplainState * pgfdw_explain_state, + PGconn *conn, + char *sql) +{ + PGresult *volatile res = NULL; + StringInfoData explain_sql; + + PG_TRY(); + { + int numrows, + i; + PgFdwExplainRemotePlans *explain = (PgFdwExplainRemotePlans *) palloc(sizeof(PgFdwExplainRemotePlans)); + + initStringInfo(&explain_sql); + initStringInfo(&explain->explain_plan); + + appendStringInfo(&explain_sql, "EXPLAIN (\ + GENERIC_PLAN 1, \ + FORMAT %s, \ + VERBOSE %d, \ + COSTS %d, \ + SETTINGS %d) \ + %s", + explain_formats[es->format], + (es->verbose) ? 1 : 0, + (es->costs) ? 1 : 0, + (es->settings) ? 1 : 0, + sql); + + /* Run the query and collect the remote plan */ + res = pgfdw_exec_query(conn, explain_sql.data, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, explain_sql.data); + + numrows = PQntuples(res); + + for (i = 0; i < numrows; i++) + appendStringInfo(&explain->explain_plan, "%s\n", pstrdup(PQgetvalue(res, i, 0))); + + if (explain->explain_plan.len > 0 && explain->explain_plan.data[explain->explain_plan.len - 1] == '\n') + explain->explain_plan.data[--explain->explain_plan.len] = '\0'; + + explain->plan_node_id = plan_node_id; + pgfdw_explain_state->all_remote_plans = lappend(pgfdw_explain_state->all_remote_plans, explain); + } + PG_FINALLY(); + { + if (res) + PQclear(res); + + if (explain_sql.data) + pfree(explain_sql.data); + } + PG_END_TRY(); +} + /* * postgresExplainForeignScan * Produce extra output for EXPLAIN of a ForeignScan on a foreign table @@ -2831,6 +2905,9 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) { ForeignScan *plan = castNode(ForeignScan, node->ss.ps.plan); List *fdw_private = plan->fdw_private; + PgFdwExplainState *pgfdw_explain_state; + char *sql; + List *foreign_scan_table = NIL; /* * Identify foreign scans that are really joins or upper relations. The @@ -2892,6 +2969,14 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) Assert(rte->rtekind == RTE_RELATION); /* This logic should agree with explain.c's ExplainTargetRel */ relname = get_rel_name(rte->relid); + + /* + * add one of the tables to foreign_scan_table to get the + * serverId for remote plans + */ + if (list_length(foreign_scan_table) == 0) + foreign_scan_table = lappend_oid(foreign_scan_table, rte->relid); + if (es->verbose) { char *namespace; @@ -2917,15 +3002,38 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) ExplainPropertyText("Relations", relations.data, es); } + sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); + /* * Add remote query, when VERBOSE option is specified. */ if (es->verbose) + ExplainPropertyText("Remote SQL", sql, es); + + pgfdw_explain_state = GetExplainExtensionState(es, GET_EXTENSION_ID()); + + if (pgfdw_explain_state && pgfdw_explain_state->remote_plans) { - char *sql; + UserMapping *user = NULL; + PGconn *conn = NULL; + ForeignTable *table; - sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); - ExplainPropertyText("Remote SQL", sql, es); + if (node && !node->ss.ss_currentRelation && + foreign_scan_table == NIL) + return; + + if (node && node->ss.ss_currentRelation) + table = GetForeignTable(RelationGetRelid(node->ss.ss_currentRelation)); + else + table = GetForeignTable(list_nth_oid(foreign_scan_table, 0)); + + Assert(table); + + user = GetUserMapping(GetUserId(), table->serverid); + conn = GetConnection(user, false, NULL); + + postgresExplainStatement(node->ss.ps.plan->plan_node_id, es, pgfdw_explain_state, conn, sql); + ReleaseConnection(conn); } } @@ -2940,11 +3048,12 @@ postgresExplainForeignModify(ModifyTableState *mtstate, int subplan_index, ExplainState *es) { + char *sql = strVal(list_nth(fdw_private, + FdwModifyPrivateUpdateSql)); + PgFdwExplainState *pgfdw_explain_state; + if (es->verbose) { - char *sql = strVal(list_nth(fdw_private, - FdwModifyPrivateUpdateSql)); - ExplainPropertyText("Remote SQL", sql, es); /* @@ -2954,6 +3063,24 @@ postgresExplainForeignModify(ModifyTableState *mtstate, if (rinfo->ri_BatchSize > 0) ExplainPropertyInteger("Batch Size", NULL, rinfo->ri_BatchSize, es); } + + pgfdw_explain_state = GetExplainExtensionState(es, GET_EXTENSION_ID()); + if (pgfdw_explain_state && pgfdw_explain_state->remote_plans) + { + UserMapping *user = NULL; + PGconn *conn = NULL; + ForeignTable *table; + + table = GetForeignTable(rinfo->ri_RelationDesc->rd_rel->oid); + + Assert(table); + + user = GetUserMapping(GetUserId(), table->serverid); + conn = GetConnection(user, false, NULL); + + postgresExplainStatement(mtstate->ps.plan->plan_node_id, es, pgfdw_explain_state, conn, sql); + ReleaseConnection(conn); + } } /* @@ -2966,12 +3093,31 @@ postgresExplainDirectModify(ForeignScanState *node, ExplainState *es) { List *fdw_private; char *sql; + PgFdwExplainState *pgfdw_explain_state; + + fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; + sql = strVal(list_nth(fdw_private, FdwDirectModifyPrivateUpdateSql)); if (es->verbose) - { - fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; - sql = strVal(list_nth(fdw_private, FdwDirectModifyPrivateUpdateSql)); ExplainPropertyText("Remote SQL", sql, es); + + pgfdw_explain_state = GetExplainExtensionState(es, GET_EXTENSION_ID()); + + if (pgfdw_explain_state && pgfdw_explain_state->remote_plans) + { + UserMapping *user = NULL; + PGconn *conn = NULL; + ForeignTable *table; + + table = GetForeignTable(RelationGetRelid(node->ss.ss_currentRelation)); + + Assert(table); + + user = GetUserMapping(GetUserId(), table->serverid); + conn = GetConnection(user, false, NULL); + + postgresExplainStatement(node->ss.ps.plan->plan_node_id, es, pgfdw_explain_state, conn, sql); + ReleaseConnection(conn); } } @@ -7886,3 +8032,95 @@ get_batch_size_option(Relation rel) return batch_size; } + +void +postgresExplainPerNode(PlanState *planstate, List *ancestors, + const char *relationship, const char *plan_name, + ExplainState *es) +{ + PgFdwExplainState *pgfdw_explain_state; + + pgfdw_explain_state = GetExplainExtensionState(es, GET_EXTENSION_ID()); + + if (pgfdw_explain_state == NULL || + !pgfdw_explain_state->remote_plans) + return; + + if (pgfdw_explain_state && pgfdw_explain_state->remote_plans) + ExplainPropertyInteger("Plan Node ID", NULL, planstate->plan->plan_node_id, es); +} + +static void +pgfdwFormatRemotePlan(PgFdwExplainRemotePlans * explain, + ExplainState *es, + int plan_node_id) +{ + char *token; + StringInfoData remote_plan_name; + + initStringInfo(&remote_plan_name); + appendStringInfo(&remote_plan_name, "Plan Node ID %d", plan_node_id); + + ExplainOpenGroup(remote_plan_name.data, remote_plan_name.data, false, es); + + if (es->format == EXPLAIN_FORMAT_TEXT) + { + appendStringInfo(es->str, "Plan Node ID %d:", plan_node_id); + appendStringInfoString(es->str, "\n"); + } + + while ((token = strsep(&explain->explain_plan.data, "\n")) != NULL) + { + if (es->format == EXPLAIN_FORMAT_JSON || + es->format == EXPLAIN_FORMAT_YAML) + appendStringInfoString(es->str, "\n"); + + appendStringInfoSpaces(es->str, (es->indent == 0) ? 2 : es->indent * 2); + appendStringInfoString(es->str, token); + + if (es->format == EXPLAIN_FORMAT_XML || + es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es->str, "\n"); + } + + ExplainCloseGroup(remote_plan_name.data, remote_plan_name.data, false, es); + pfree(remote_plan_name.data); +} + +void +postgresExplainPerPlan(PlannedStmt *plannedstmt, + IntoClause *into, + ExplainState *es, + const char *queryString, + ParamListInfo params, + QueryEnvironment *queryEnv) +{ + ListCell *lc; + PgFdwExplainState *pgfdw_explain_state; + + pgfdw_explain_state = GetExplainExtensionState(es, GET_EXTENSION_ID()); + + if (pgfdw_explain_state == NULL || + pgfdw_explain_state->all_remote_plans == NIL || + !pgfdw_explain_state->remote_plans) + return; + + ExplainOpenGroup("Remote Plans", "Remote Plans", true, es); + if (es->format == EXPLAIN_FORMAT_TEXT) + { + appendStringInfo(es->str, "Remote Plans:\n"); + appendStringInfo(es->str, "-------------\n"); + } + + /* Process every remote plan captured */ + foreach(lc, pgfdw_explain_state->all_remote_plans) + { + PgFdwExplainRemotePlans *explain = (PgFdwExplainRemotePlans *) lfirst(lc); + + pgfdwFormatRemotePlan(explain, + es, + explain->plan_node_id); + } + + ExplainCloseGroup("Remote Plans", "Remote Plans", true, es); +} diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index e69735298d7..b135664b933 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -13,6 +13,7 @@ #ifndef POSTGRES_FDW_H #define POSTGRES_FDW_H +#include "commands/explain_state.h" #include "foreign/foreign.h" #include "lib/stringinfo.h" #include "libpq/libpq-be-fe.h" @@ -151,6 +152,21 @@ typedef enum PgFdwSamplingMethod ANALYZE_SAMPLE_BERNOULLI, /* TABLESAMPLE bernoulli */ } PgFdwSamplingMethod; +typedef struct PgFdwExplainRemotePlans +{ + int plan_node_id; + StringInfoData explain_plan; + +} PgFdwExplainRemotePlans; + +typedef struct PgFdwExplainState +{ + List *all_remote_plans; + + /* EXPLAIN options */ + bool remote_plans; +} PgFdwExplainState; + /* in postgres_fdw.c */ extern int set_transmission_modes(void); extern void reset_transmission_modes(int nestlevel); @@ -178,6 +194,16 @@ extern int ExtractConnectionOptions(List *defelems, extern List *ExtractExtensionList(const char *extensionsString, bool warnOnMissing); extern char *process_pgfdw_appname(const char *appname); +extern void postgresExplainPerNode(PlanState *planstate, List *ancestors, + const char *relationship, + const char *plan_name, + ExplainState *es); +extern void postgresExplainPerPlan(PlannedStmt *plannedstmt, + IntoClause *into, + ExplainState *es, + const char *queryString, + ParamListInfo params, + QueryEnvironment *queryEnv); extern char *pgfdw_application_name; /* in deparse.c */ diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 9a8f9e28135..25823a7ebe7 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -281,6 +281,11 @@ SELECT * FROM ft1 t1 WHERE t1.c3 = (SELECT MAX(c3) FROM ft2 t2) ORDER BY c1; WITH t1 AS (SELECT * FROM ft1 WHERE c1 <= 10) SELECT t2.c1, t2.c2, t2.c3, t2.c4 FROM t1, ft2 t2 WHERE t1.c1 = t2.c1 ORDER BY t1.c1; -- fixed values SELECT 'fixed', NULL FROM ft1 t1 WHERE c1 = 1; +-- with WHERE clause and remote_plans with different formats +EXPLAIN (REMOTE_PLANS, FORMAT YAML, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; +EXPLAIN (REMOTE_PLANS TRUE, FORMAT XML, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; +EXPLAIN (REMOTE_PLANS, FORMAT JSON, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; +EXPLAIN (REMOTE_PLANS TRUE, FORMAT TEXT, VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101; -- Test forcing the remote server to produce sorted data for a merge join. SET enable_hashjoin TO false; SET enable_nestloop TO false; @@ -1489,6 +1494,33 @@ SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE SELECT ft2.c1 FROM ft2 JOIN ft4 ON ft2.c1 = ft4.c1) ORDER BY ft1.c1 LIMIT 5; +-- EXPLAIN remote_plans +EXPLAIN (remote_plans, format text, costs off, analyze) +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; +EXPLAIN (remote_plans, format text, 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; +EXPLAIN (remote_plans, format xml, 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; +EXPLAIN (remote_plans, format json, 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; +EXPLAIN (remote_plans, format yaml, 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 -- =================================================================== @@ -1538,6 +1570,10 @@ EXPLAIN (verbose, costs off) DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass; -- can be pushed down DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass; +-- test write on foreign tables with remote_plans +EXPLAIN (remote_plans, verbose, costs off) +UPDATE ft2 SET c2 = c2 + 300 WHERE c1 % 10 = 3; + -- Test UPDATE/DELETE with RETURNING on a three-table join INSERT INTO ft2 (c1,c2,c3) SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id; @@ -4138,6 +4174,9 @@ INSERT INTO insert_tbl (SELECT * FROM local_tbl UNION ALL SELECT * FROM remote_t SELECT * FROM insert_tbl ORDER BY a; +EXPLAIN (REMOTE_PLANS, VERBOSE, COSTS OFF) +INSERT INTO insert_tbl (SELECT * FROM local_tbl UNION ALL SELECT * FROM remote_tbl); + -- Check with direct modify EXPLAIN (VERBOSE, COSTS OFF) WITH t AS (UPDATE remote_tbl SET c = c || c RETURNING *) -- 2.43.0