Today you get

test=> EXPLAIN SELECT * FROM tab WHERE col = $1;
ERROR:  there is no parameter $1

which makes sense.  Nonetheless, it would be great to get a generic plan
for such a query.  Sometimes you don't have the parameters (if you grab
the statement from "pg_stat_statements", or if it is from an error message
in the log, and you didn't enable "log_parameter_max_length_on_error").
Sometimes it is just very painful to substitute the 25 parameters from
the detail message.

With the attached patch you can get the following:

test=> SET plan_cache_mode = force_generic_plan;
SET
test=> EXPLAIN (COSTS OFF) SELECT * FROM pg_proc WHERE oid = $1;
                  QUERY PLAN                   
═══════════════════════════════════════════════
 Index Scan using pg_proc_oid_index on pg_proc
   Index Cond: (oid = $1)
(2 rows)

That's not the same as a full-fledged EXPLAIN (ANALYZE, BUFFERS),
but it can definitely be helpful.

I tied that behavior to the setting of "plan_cache_mode" where you
are guaranteed to get a generic plan; I couldn't think of a better way.

Yours,
Laurenz Albe
From 2bc91581acd478d4648176b58745cadb835d5fbc Mon Sep 17 00:00:00 2001
From: Laurenz Albe <laurenz.a...@cybertec.at>
Date: Tue, 11 Oct 2022 13:05:31 +0200
Subject: [PATCH] Add EXPLAIN support for parameterized statements

If "plan_cache_mode = force_generic_plan", allow EXPLAIN to
generate generic plans for parameterized statements (that
have parameter placeholders like $1 in the statement text).

This repurposes hooks used by PL/pgSQL, so we better not try
to do that inside PL/pgSQL.
---
 doc/src/sgml/ref/explain.sgml         | 10 +++++
 src/backend/parser/analyze.c          | 53 +++++++++++++++++++++++++++
 src/test/regress/expected/explain.out | 28 ++++++++++++++
 src/test/regress/sql/explain.sql      | 13 +++++++
 4 files changed, 104 insertions(+)

diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index d4895b9d7d..928d67b9b4 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -321,6 +321,16 @@ ROLLBACK;
    execution, and on machines that have relatively slow operating
    system calls for obtaining the time of day.
   </para>
+
+  <para>
+   If <xref linkend="guc-plan-cache_mode"/> is set to
+   <literal>force_generic_plan</literal>, you can use <command>EXPLAIN</command>
+   to generate generic plans for statements that contain placeholders like
+   <literal>$1</literal> without knowing the actual parameter type or value.
+   Note that expressions like <literal>$1 + $2</literal> are ambiguous if you
+   don't specify the parameter data types, so you may have to add explicit type
+   casts in such cases.
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 6688c2a865..c481d45376 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -52,6 +52,7 @@
 #include "utils/guc.h"
 #include "utils/queryjumble.h"
 #include "utils/rel.h"
+#include "utils/plancache.h"
 #include "utils/syscache.h"
 
 
@@ -86,6 +87,10 @@ static Query *transformCallStmt(ParseState *pstate,
 								CallStmt *stmt);
 static void transformLockingClause(ParseState *pstate, Query *qry,
 								   LockingClause *lc, bool pushedDown);
+static Node * fakeUnknownParam(ParseState *pstate, ParamRef *pref);
+static Node * coerceUnknownParam(ParseState *pstate, Param *param,
+								 Oid targetTypeId, int32 targetTypeMod,
+								 int location);
 #ifdef RAW_EXPRESSION_COVERAGE_TEST
 static bool test_raw_expression_coverage(Node *node, void *context);
 #endif
@@ -2895,6 +2900,22 @@ transformExplainStmt(ParseState *pstate, ExplainStmt *stmt)
 {
 	Query	   *result;
 
+	/*
+	 * If we EXPLAIN a statement and are certain to generate a generic plan,
+	 * we can tolerate undefined parameters.  For that purpose, supply
+	 * parameters of type "unknown" and coerce them to the appropriate type
+	 * as needed.
+	 * If we are called from PL/pgSQL, the hooks are already set for the
+	 * purpose of resolving variables, and we don't want to disturb that.
+	 */
+	if (plan_cache_mode == PLAN_CACHE_MODE_FORCE_GENERIC_PLAN &&
+		pstate->p_paramref_hook == NULL &&
+		pstate->p_coerce_param_hook == NULL)
+	{
+		pstate->p_paramref_hook = fakeUnknownParam;
+		pstate->p_coerce_param_hook = coerceUnknownParam;
+	}
+
 	/* transform contained query, allowing SELECT INTO */
 	stmt->query = (Node *) transformOptionalSelectInto(pstate, stmt->query);
 
@@ -3466,6 +3487,38 @@ applyLockingClause(Query *qry, Index rtindex,
 	qry->rowMarks = lappend(qry->rowMarks, rc);
 }
 
+/*
+ * Return an "unknown" parameter for use with EXPLAIN of a parameterized
+ * statement.
+ */
+Node *
+fakeUnknownParam(ParseState *pstate, ParamRef *pref)
+{
+	Param  *param;
+
+	param = makeNode(Param);
+	param->paramkind = PARAM_EXTERN;
+	param->paramid = pref->number;
+	param->paramtype = UNKNOWNOID;
+	param->paramtypmod = -1;
+	param->paramcollid = InvalidOid;
+	param->location = pref->location;
+
+	return (Node *)param;
+}
+
+/*
+ * Set the parameter's type from "unknown" to the target type.
+ */
+Node *
+coerceUnknownParam(ParseState *pstate, Param *param, Oid targetTypeId,
+				   int32 targetTypeMod, int location)
+{
+	param->paramtype = targetTypeId;
+
+	return (Node *)param;
+}
+
 /*
  * Coverage testing for raw_expression_tree_walker().
  *
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 48620edbc2..4f6755b172 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -517,3 +517,31 @@ select explain_filter('explain (verbose) select * from int8_tbl i8');
  Query Identifier: N
 (3 rows)
 
+reset compute_query_id;
+-- Test parameterized plans
+-- it will fail unless plan_cache_mode = force_generic_plan
+explain (costs off) select unique1 from tenk1 where thousand = $1;
+ERROR:  there is no parameter $1
+LINE 1: ...n (costs off) select unique1 from tenk1 where thousand = $1;
+                                                                    ^
+set plan_cache_mode=force_generic_plan;
+explain (costs off) select unique1 from tenk1 where thousand = $1;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Bitmap Heap Scan on tenk1
+   Recheck Cond: (thousand = $1)
+   ->  Bitmap Index Scan on tenk1_thous_tenthous
+         Index Cond: (thousand = $1)
+(4 rows)
+
+-- it should also fail from PL/pgSQL
+do $_$declare
+  x text;
+begin
+  explain (costs off) select unique1 from tenk1 where thousand = $1 into x;
+end;$_$;
+ERROR:  there is no parameter $1
+LINE 1: ...in (costs off) select unique1 from tenk1 where thousand = $1
+                                                                     ^
+QUERY:  explain (costs off) select unique1 from tenk1 where thousand = $1
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index ae3f7a308d..9d4827d299 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -128,3 +128,16 @@ select explain_filter('explain (verbose) select * from t1 where pg_temp.mysin(f1
 -- Test compute_query_id
 set compute_query_id = on;
 select explain_filter('explain (verbose) select * from int8_tbl i8');
+reset compute_query_id;
+
+-- Test parameterized plans
+-- it will fail unless plan_cache_mode = force_generic_plan
+explain (costs off) select unique1 from tenk1 where thousand = $1;
+set plan_cache_mode=force_generic_plan;
+explain (costs off) select unique1 from tenk1 where thousand = $1;
+-- it should also fail from PL/pgSQL
+do $_$declare
+  x text;
+begin
+  explain (costs off) select unique1 from tenk1 where thousand = $1 into x;
+end;$_$;
-- 
2.37.3

Reply via email to