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