Thread: Make EXPLAIN generate a generic plan for a parameterized query

Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
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

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Tom Lane
Date:
Laurenz Albe <laurenz.albe@cybertec.at> writes:
> 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.

I can see the point, but it also seems like it risks masking stupid
mistakes.

> 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.

I think it might be better to drive it off an explicit EXPLAIN option,
perhaps

EXPLAIN (GENERIC_PLAN) SELECT * FROM tab WHERE col = $1;

This option (bikeshedding on the name welcome) would have the effect
both of allowing unanchored Param symbols and of temporarily forcing
generic-plan mode, so that you don't need additional commands to
set and reset plan_cache_mode.  We could also trivially add logic
to disallow the combination of ANALYZE and GENERIC_PLAN, which
would otherwise be a bit messy to prevent.

For context, it does already work to do this when you want to
investigate parameterized plans:

regression=# prepare foo as select * from tenk1 where unique1 = $1;
PREPARE
regression=# explain execute foo(42);
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Index Scan using tenk1_unique1 on tenk1  (cost=0.29..8.30 rows=1 width=244)
   Index Cond: (unique1 = 42)
(2 rows)

If you're trying to investigate custom-plan behavior, then you
need to supply concrete parameter values somewhere, so I think
this approach is fine for that case.  (Shoehorning parameter
values into EXPLAIN options seems like it'd be a bit much.)
However, investigating generic-plan behavior this way is tedious,
since you have to invent irrelevant parameter values, plus mess
with plan_cache_mode or else run the explain half a dozen times.
So I can get behind having a more convenient way for that.

            regards, tom lane



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Julien Rouhaud
Date:
On Tue, Oct 11, 2022 at 09:49:14AM -0400, Tom Lane wrote:
>
> If you're trying to investigate custom-plan behavior, then you
> need to supply concrete parameter values somewhere, so I think
> this approach is fine for that case.  (Shoehorning parameter
> values into EXPLAIN options seems like it'd be a bit much.)
> However, investigating generic-plan behavior this way is tedious,
> since you have to invent irrelevant parameter values, plus mess
> with plan_cache_mode or else run the explain half a dozen times.
> So I can get behind having a more convenient way for that.

One common use case is tools identifying a slow query using pg_stat_statements,
identifying some missing indexes and then wanting to check whether the index
should be useful using some hypothetical index.

FTR I'm working on such a project and for now we have to go to great lengths
trying to "unjumble" such queries, so having a way to easily get the answer for
a generic plan would be great.



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Wed, 2022-10-12 at 00:03 +0800, Julien Rouhaud wrote:
> On Tue, Oct 11, 2022 at 09:49:14AM -0400, Tom Lane wrote:
> > I think it might be better to drive it off an explicit EXPLAIN option,
> > perhaps
> >
> > EXPLAIN (GENERIC_PLAN) SELECT * FROM tab WHERE col = $1;
> > 
> > If you're trying to investigate custom-plan behavior, then you
> > need to supply concrete parameter values somewhere, so I think
> > this approach is fine for that case.  (Shoehorning parameter
> > values into EXPLAIN options seems like it'd be a bit much.)
> > However, investigating generic-plan behavior this way is tedious,
> > since you have to invent irrelevant parameter values, plus mess
> > with plan_cache_mode or else run the explain half a dozen times.
> > So I can get behind having a more convenient way for that.
> 
> One common use case is tools identifying a slow query using pg_stat_statements,
> identifying some missing indexes and then wanting to check whether the index
> should be useful using some hypothetical index.
> 
> FTR I'm working on such a project and for now we have to go to great lengths
> trying to "unjumble" such queries, so having a way to easily get the answer for
> a generic plan would be great.

Thanks for the suggestions and the encouragement.  Here is a patch that
implements it with an EXPLAIN option named GENERIC_PLAN.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Julien Rouhaud
Date:
Hi,

On Tue, Oct 25, 2022 at 11:08:27AM +0200, Laurenz Albe wrote:
>
> Here is a patch that
> implements it with an EXPLAIN option named GENERIC_PLAN.

I only have a quick look at the patch for now.  Any reason why you don't rely
on the existing explain_filter() function for emitting stable output (without
having to remove the costs)?  It would also take care of checking that it works
in plpgsql.



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Tue, 2022-10-25 at 19:03 +0800, Julien Rouhaud wrote:
> On Tue, Oct 25, 2022 at 11:08:27AM +0200, Laurenz Albe wrote:
> > Here is a patch that
> > implements it with an EXPLAIN option named GENERIC_PLAN.
> 
> I only have a quick look at the patch for now.  Any reason why you don't rely
> on the existing explain_filter() function for emitting stable output (without
> having to remove the costs)?  It would also take care of checking that it works
> in plpgsql.

No, there is no principled reason I did it like that.  Version 2 does it like
you suggest.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Andres Freund
Date:
Hi,

On 2022-10-29 10:35:26 +0200, Laurenz Albe wrote:
> On Tue, 2022-10-25 at 19:03 +0800, Julien Rouhaud wrote:
> > On Tue, Oct 25, 2022 at 11:08:27AM +0200, Laurenz Albe wrote:
> > > Here is a patch that
> > > implements it with an EXPLAIN option named GENERIC_PLAN.
> > 
> > I only have a quick look at the patch for now.  Any reason why you don't rely
> > on the existing explain_filter() function for emitting stable output (without
> > having to remove the costs)?  It would also take care of checking that it works
> > in plpgsql.
> 
> No, there is no principled reason I did it like that.  Version 2 does it like
> you suggest.

This fails to build the docs:

https://cirrus-ci.com/task/5609301511766016

[17:47:01.064] ref/explain.sgml:179: parser error : Opening and ending tag mismatch: likeral line 179 and literal
[17:47:01.064]       <likeral>ANALYZE</literal>, since a statement with unknown parameters
[17:47:01.064]                                 ^

Greetings,

Andres Freund



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Tue, 2022-12-06 at 10:17 -0800, Andres Freund wrote:
> On 2022-10-29 10:35:26 +0200, Laurenz Albe wrote:
> > > > Here is a patch that
> > > > implements it with an EXPLAIN option named GENERIC_PLAN.
> 
> This fails to build the docs:
> 
> https://cirrus-ci.com/task/5609301511766016
> 
> [17:47:01.064] ref/explain.sgml:179: parser error : Opening and ending tag mismatch: likeral line 179 and literal
> [17:47:01.064]       <likeral>ANALYZE</literal>, since a statement with unknown parameters
> [17:47:01.064]                                 ^

*blush* Here is a fixed version.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Michel Pelletier
Date:
On Wed, Dec 7, 2022 at 3:23 AM Laurenz Albe <laurenz.albe@cybertec.at> wrote:
On Tue, 2022-12-06 at 10:17 -0800, Andres Freund wrote:
> On 2022-10-29 10:35:26 +0200, Laurenz Albe wrote:
> > > > Here is a patch that
> > > > implements it with an EXPLAIN option named GENERIC_PLAN.
>
> This fails to build the docs:
>
> https://cirrus-ci.com/task/5609301511766016
>
> [17:47:01.064] ref/explain.sgml:179: parser error : Opening and ending tag mismatch: likeral line 179 and literal
> [17:47:01.064]       <likeral>ANALYZE</literal>, since a statement with unknown parameters
> [17:47:01.064]                                 ^

*blush* Here is a fixed version.

I built and tested this patch for review and it works well, although I got the following warning when building:

analyze.c: In function 'transformStmt':
analyze.c:2919:35: warning: 'generic_plan' may be used uninitialized in this function [-Wmaybe-uninitialized]
 2919 |         pstate->p_generic_explain = generic_plan;
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~
analyze.c:2909:25: note: 'generic_plan' was declared here
 2909 |         bool            generic_plan;
      |                         ^~~~~~~~~~~~

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Tue, 2022-12-27 at 14:37 -0800, Michel Pelletier wrote:
> I built and tested this patch for review and it works well, although I got the following warning when building:
>
> analyze.c: In function 'transformStmt':
> analyze.c:2919:35: warning: 'generic_plan' may be used uninitialized in this function [-Wmaybe-uninitialized]
>  2919 |         pstate->p_generic_explain = generic_plan;
>       |         ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~
> analyze.c:2909:25: note: 'generic_plan' was declared here
>  2909 |         bool            generic_plan;
>       |                         ^~~~~~~~~~~~

Thanks for checking.  The variable should indeed be initialized, although
my compiler didn't complain.

Attached is a fixed version.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Jim Jones
Date:
Hi Laurenz,

I'm testing your patch and the GENERIC_PLAN parameter seems to work just 
OK ..

db=# CREATE TABLE t (col numeric);
CREATE TABLE
db=# CREATE INDEX t_col_idx ON t (col);
CREATE INDEX
db=# INSERT INTO t SELECT random() FROM generate_series(1,100000) ;
INSERT 0 100000
db=# EXPLAIN (GENERIC_PLAN) SELECT * FROM t WHERE col = $1;
                                 QUERY PLAN
---------------------------------------------------------------------------
  Bitmap Heap Scan on t  (cost=15.27..531.67 rows=368 width=32)
    Recheck Cond: (col = $1)
    ->  Bitmap Index Scan on t_col_idx  (cost=0.00..15.18 rows=368 width=0)
          Index Cond: (col = $1)
(4 rows)


.. the error message when combining GENERIC_PLAN with ANALYSE also works 
as expected

db=# EXPLAIN (ANALYSE, GENERIC_PLAN) SELECT * FROM t WHERE col = $1;
ERROR:  EXPLAIN ANALYZE cannot be used with GENERIC_PLAN

.. and the system also does not throw an error when it's used along 
other parameters, e.g. VERBOSE, WAL, SUMMARY, etc.

However, when GENERIC_PLAN is used combined with BUFFERS, the 'Buffers' 
node is shown the first time the query executed in a session:

psql (16devel)
Type "help" for help.

postgres=# \c db
You are now connected to database "db" as user "postgres".
db=# EXPLAIN (BUFFERS, GENERIC_PLAN) SELECT * FROM t WHERE col = $1;
                                QUERY PLAN
-------------------------------------------------------------------------
  Index Only Scan using t_col_idx on t  (cost=0.42..4.44 rows=1 width=11)
    Index Cond: (col = $1)
  Planning:
    Buffers: shared hit=62
(4 rows)

db=# EXPLAIN (BUFFERS, GENERIC_PLAN) SELECT * FROM t WHERE col = $1;
                                QUERY PLAN
-------------------------------------------------------------------------
  Index Only Scan using t_col_idx on t  (cost=0.42..4.44 rows=1 width=11)
    Index Cond: (col = $1)
(2 rows)

db=# EXPLAIN (BUFFERS, GENERIC_PLAN) SELECT * FROM t WHERE col = $1;
                                QUERY PLAN
-------------------------------------------------------------------------
  Index Only Scan using t_col_idx on t  (cost=0.42..4.44 rows=1 width=11)
    Index Cond: (col = $1)
(2 rows)

Is it the expected behaviour?

Also, this new parameter seems only to work between parenthesis 
`(GENERIC_PLAN)`:

db=# EXPLAIN GENERIC_PLAN SELECT * FROM t WHERE col = $1;
ERROR:  syntax error at or near "GENERIC_PLAN"
LINE 1: EXPLAIN GENERIC_PLAN SELECT * FROM t WHERE col = $1;


If it's intended to be consistent with the other "single parameters", 
perhaps it should work also without parenthesis? e.g.

db=# EXPLAIN ANALYSE SELECT * FROM t WHERE col < 0.42;
                                                           QUERY PLAN

------------------------------------------------------------------------------------------------------------------------------
  Index Only Scan using t_col_idx on t  (cost=0.42..1637.25 rows=41876 
width=11) (actual time=0.103..6.293 rows=41932 loops=1)
    Index Cond: (col < 0.42)
    Heap Fetches: 0
  Planning Time: 0.071 ms
  Execution Time: 7.316 ms
(5 rows)


db=# EXPLAIN VERBOSE SELECT * FROM t WHERE col < 0.42;
                                       QUERY PLAN
---------------------------------------------------------------------------------------
  Index Only Scan using t_col_idx on public.t (cost=0.42..1637.25 
rows=41876 width=11)
    Output: col
    Index Cond: (t.col < 0.42)
(3 rows)


On a very personal note: wouldn't just GENERIC (without _PLAN) suffice? 
Don't bother with it if you disagree :-)

Cheers
Jim

On 09.01.23 17:40, Laurenz Albe wrote:
> On Tue, 2022-12-27 at 14:37 -0800, Michel Pelletier wrote:
>> I built and tested this patch for review and it works well, although I got the following warning when building:
>>
>> analyze.c: In function 'transformStmt':
>> analyze.c:2919:35: warning: 'generic_plan' may be used uninitialized in this function [-Wmaybe-uninitialized]
>>   2919 |         pstate->p_generic_explain = generic_plan;
>>        |         ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~
>> analyze.c:2909:25: note: 'generic_plan' was declared here
>>   2909 |         bool            generic_plan;
>>        |                         ^~~~~~~~~~~~
> Thanks for checking.  The variable should indeed be initialized, although
> my compiler didn't complain.
>
> Attached is a fixed version.
>
> Yours,
> Laurenz Albe



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Tom Lane
Date:
Jim Jones <jim.jones@uni-muenster.de> writes:
> However, when GENERIC_PLAN is used combined with BUFFERS, the 'Buffers' 
> node is shown the first time the query executed in a session:

> psql (16devel)
> Type "help" for help.

> postgres=# \c db
> You are now connected to database "db" as user "postgres".
> db=# EXPLAIN (BUFFERS, GENERIC_PLAN) SELECT * FROM t WHERE col = $1;
>                                 QUERY PLAN
> -------------------------------------------------------------------------
>   Index Only Scan using t_col_idx on t  (cost=0.42..4.44 rows=1 width=11)
>     Index Cond: (col = $1)
>   Planning:
>     Buffers: shared hit=62
> (4 rows)

> db=# EXPLAIN (BUFFERS, GENERIC_PLAN) SELECT * FROM t WHERE col = $1;
>                                 QUERY PLAN
> -------------------------------------------------------------------------
>   Index Only Scan using t_col_idx on t  (cost=0.42..4.44 rows=1 width=11)
>     Index Cond: (col = $1)
> (2 rows)

That's not new to this patch, the same thing happens without it.
It's reflecting catalog accesses involved in loading per-session
caches (which, therefore, needn't be repeated the second time).

> Also, this new parameter seems only to work between parenthesis 
> `(GENERIC_PLAN)`:

> db=# EXPLAIN GENERIC_PLAN SELECT * FROM t WHERE col = $1;
> ERROR:  syntax error at or near "GENERIC_PLAN"
> LINE 1: EXPLAIN GENERIC_PLAN SELECT * FROM t WHERE col = $1;

That's true of all but the oldest EXPLAIN options.  We don't do that
anymore because new options would have to become grammar keywords
to support that.

> On a very personal note: wouldn't just GENERIC (without _PLAN) suffice? 
> Don't bother with it if you disagree :-)

FWIW, "GENERIC" would be too generic for my taste ;-).  But I agree
it's a judgement call.

            regards, tom lane



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Tom Lane
Date:
Laurenz Albe <laurenz.albe@cybertec.at> writes:
> [ 0001-Add-EXPLAIN-option-GENERIC_PLAN.v4.patch ]

I took a closer look at this patch, and didn't like the implementation
much.  You're not matching the behavior of PREPARE at all: for example,
this patch is content to let $1 be resolved with different types in
different places.  We should be using the existing infrastructure that
parse_analyze_varparams uses.

Also, I believe that in contexts such as plpgsql, it is possible that
there's an external source of $N definitions, which we should probably
continue to honor even with GENERIC_PLAN.

So that leads me to think the code should be more like this.  I'm not
sure if it's worth spending documentation and testing effort on the
case where we don't override an existing p_paramref_hook.

            regards, tom lane

diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index d4895b9d7d..58350624e7 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -40,6 +40,7 @@ EXPLAIN [ ANALYZE ] [ VERBOSE ] <replaceable class="parameter">statement</replac
     VERBOSE [ <replaceable class="parameter">boolean</replaceable> ]
     COSTS [ <replaceable class="parameter">boolean</replaceable> ]
     SETTINGS [ <replaceable class="parameter">boolean</replaceable> ]
+    GENERIC_PLAN [ <replaceable class="parameter">boolean</replaceable> ]
     BUFFERS [ <replaceable class="parameter">boolean</replaceable> ]
     WAL [ <replaceable class="parameter">boolean</replaceable> ]
     TIMING [ <replaceable class="parameter">boolean</replaceable> ]
@@ -167,6 +168,20 @@ ROLLBACK;
     </listitem>
    </varlistentry>

+   <varlistentry>
+    <term><literal>GENERIC_PLAN</literal></term>
+    <listitem>
+     <para>
+      Generate a generic plan for the statement (see <xref linkend="sql-prepare"/>
+      for details about generic plans).  The statement can contain parameter
+      placeholders like <literal>$1</literal>, if it is a statement that can
+      use parameters.  This option cannot be used together with
+      <literal>ANALYZE</literal>, since a statement with unknown parameters
+      cannot be executed.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>BUFFERS</literal></term>
     <listitem>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 35c23bd27d..ab21a67862 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -190,6 +190,8 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
             es->wal = defGetBoolean(opt);
         else if (strcmp(opt->defname, "settings") == 0)
             es->settings = defGetBoolean(opt);
+        else if (strcmp(opt->defname, "generic_plan") == 0)
+            es->generic = defGetBoolean(opt);
         else if (strcmp(opt->defname, "timing") == 0)
         {
             timing_set = true;
@@ -227,6 +229,13 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
                      parser_errposition(pstate, opt->location)));
     }

+    /* check that GENERIC_PLAN is not used with EXPLAIN ANALYZE */
+    if (es->generic && es->analyze)
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                 errmsg("EXPLAIN ANALYZE cannot be used with GENERIC_PLAN")));
+
+    /* check that WAL is used with EXPLAIN ANALYZE */
     if (es->wal && !es->analyze)
         ereport(ERROR,
                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index e892df9819..9143964e78 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -27,6 +27,7 @@
 #include "access/sysattr.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "commands/defrem.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -2906,10 +2907,38 @@ static Query *
 transformExplainStmt(ParseState *pstate, ExplainStmt *stmt)
 {
     Query       *result;
+    bool        generic_plan = false;
+    Oid           *paramTypes = NULL;
+    int            numParams = 0;
+
+    /*
+     * If we have no external source of parameter definitions, and the
+     * GENERIC_PLAN option is specified, then accept variable parameter
+     * definitions (as occurs in PREPARE, for example).
+     */
+    if (pstate->p_paramref_hook == NULL)
+    {
+        ListCell   *lc;
+
+        foreach(lc, stmt->options)
+        {
+            DefElem    *opt = (DefElem *) lfirst(lc);
+
+            if (strcmp(opt->defname, "generic_plan") == 0)
+                generic_plan = defGetBoolean(opt);
+            /* don't "break", as we want the last value */
+        }
+        if (generic_plan)
+            setup_parse_variable_parameters(pstate, ¶mTypes, &numParams);
+    }

     /* transform contained query, allowing SELECT INTO */
     stmt->query = (Node *) transformOptionalSelectInto(pstate, stmt->query);

+    /* make sure all is well with parameter types */
+    if (generic_plan)
+        check_variable_parameters(pstate, (Query *) stmt->query);
+
     /* represent the command as a utility Query */
     result = makeNode(Query);
     result->commandType = CMD_UTILITY;
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 7c1071ddd1..3d3e632a0c 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -46,6 +46,7 @@ typedef struct ExplainState
     bool        timing;            /* print detailed node timing */
     bool        summary;        /* print total planning and execution timing */
     bool        settings;        /* print modified settings */
+    bool        generic;        /* generate a generic plan */
     ExplainFormat format;        /* output format */
     /* state for output formatting --- not reset for each new plan tree */
     int            indent;            /* current indentation level */
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 48620edbc2..18f7ac93c4 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -517,3 +517,17 @@ select explain_filter('explain (verbose) select * from int8_tbl i8');
  Query Identifier: N
 (3 rows)

+-- Test EXPLAIN (GENERIC_PLAN)
+select explain_filter('explain (generic_plan) select unique1 from tenk1 where thousand = $1');
+                                 explain_filter
+---------------------------------------------------------------------------------
+ Bitmap Heap Scan on tenk1  (cost=N.N..N.N rows=N width=N)
+   Recheck Cond: (thousand = $N)
+   ->  Bitmap Index Scan on tenk1_thous_tenthous  (cost=N.N..N.N rows=N width=N)
+         Index Cond: (thousand = $N)
+(4 rows)
+
+-- should fail
+select explain_filter('explain (analyze, generic_plan) select unique1 from tenk1 where thousand = $1');
+ERROR:  EXPLAIN ANALYZE cannot be used with GENERIC_PLAN
+CONTEXT:  PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index ae3f7a308d..fce031775a 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -128,3 +128,8 @@ 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');
+
+-- Test EXPLAIN (GENERIC_PLAN)
+select explain_filter('explain (generic_plan) select unique1 from tenk1 where thousand = $1');
+-- should fail
+select explain_filter('explain (analyze, generic_plan) select unique1 from tenk1 where thousand = $1');

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Tue, 2023-01-31 at 13:49 -0500, Tom Lane wrote:
> Laurenz Albe <laurenz.albe@cybertec.at> writes:
> > [ 0001-Add-EXPLAIN-option-GENERIC_PLAN.v4.patch ]
>
> I took a closer look at this patch, and didn't like the implementation
> much.  You're not matching the behavior of PREPARE at all: for example,
> this patch is content to let $1 be resolved with different types in
> different places.  We should be using the existing infrastructure that
> parse_analyze_varparams uses.
>
> Also, I believe that in contexts such as plpgsql, it is possible that
> there's an external source of $N definitions, which we should probably
> continue to honor even with GENERIC_PLAN.
>
> So that leads me to think the code should be more like this.  I'm not
> sure if it's worth spending documentation and testing effort on the
> case where we don't override an existing p_paramref_hook.

Thanks, that looks way cleaner.

I played around with it, and I ran into a problem with partitions that
are foreign tables:

  CREATE TABLE loc1 (id integer NOT NULL, key integer NOT NULL CHECK (key = 1), value text);

  CREATE TABLE loc2 (id integer NOT NULL, key integer NOT NULL CHECK (key = 2), value text);

  CREATE TABLE looppart (id integer GENERATED ALWAYS AS IDENTITY, key integer NOT NULL, value text) PARTITION BY LIST
(key);

  CREATE FOREIGN TABLE looppart1 PARTITION OF looppart FOR VALUES IN (1) SERVER loopback OPTIONS (table_name 'loc1');

  CREATE FOREIGN TABLE looppart2 PARTITION OF looppart FOR VALUES IN (2) SERVER loopback OPTIONS (table_name 'loc2');

  EXPLAIN (GENERIC_PLAN) SELECT * FROM looppart WHERE key = $1;
  ERROR:  no value found for parameter 1

The solution could be to set up a dynamic parameter hook in the
ExprContext in ecxt_param_list_info->paramFetch so that
ExecEvalParamExtern doesn't complain about the missing parameter.

Does that make sense?  How do I best hook into the executor to set that up?

Yours,
Laurenz Albe



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Tom Lane
Date:
Laurenz Albe <laurenz.albe@cybertec.at> writes:
> I played around with it, and I ran into a problem with partitions that
> are foreign tables:
> ...
>   EXPLAIN (GENERIC_PLAN) SELECT * FROM looppart WHERE key = $1;
>   ERROR:  no value found for parameter 1

Hmm, offhand I'd say that something is doing something it has no
business doing when EXEC_FLAG_EXPLAIN_ONLY is set (that is, premature
evaluation of an expression).  I wonder whether this failure is
reachable without this patch.

            regards, tom lane



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Fri, 2023-02-03 at 09:44 -0500, Tom Lane wrote:
> Laurenz Albe <laurenz.albe@cybertec.at> writes:
> > I played around with it, and I ran into a problem with partitions that
> > are foreign tables:
> > ...
> >   EXPLAIN (GENERIC_PLAN) SELECT * FROM looppart WHERE key = $1;
> >   ERROR:  no value found for parameter 1
>
> Hmm, offhand I'd say that something is doing something it has no
> business doing when EXEC_FLAG_EXPLAIN_ONLY is set (that is, premature
> evaluation of an expression).  I wonder whether this failure is
> reachable without this patch.

Thanks for the pointer.  Perhaps something like the attached?
The changes in "CreatePartitionPruneState" make my test case work,
but they cause a difference in the regression tests, which would be
intended if we have no partition pruning with plain EXPLAIN.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Tom Lane
Date:
Laurenz Albe <laurenz.albe@cybertec.at> writes:
> Thanks for the pointer.  Perhaps something like the attached?
> The changes in "CreatePartitionPruneState" make my test case work,
> but they cause a difference in the regression tests, which would be
> intended if we have no partition pruning with plain EXPLAIN.

Hmm, so I see (and the cfbot entry for this patch is now all red,
because you didn't include that diff in the patch).

I'm not sure if we can get away with that behavioral change.
People are probably expecting the current behavior for existing
cases.

I can think of a couple of possible ways forward:

* Fix things so that the generic parameters appear to have NULL
values when inspected during executor startup.  I'm not sure
how useful that'd be though.  In partition-pruning cases that'd
lead to EXPLAIN (GENERIC_PLAN) showing the plan with all
partitions pruned, other than the one for NULL values if there
is one.  Doesn't sound too helpful.

* Invent another executor flag that's a "stronger" version of
EXEC_FLAG_EXPLAIN_ONLY, and pass that when any generic parameters
exist, and check it in CreatePartitionPruneState to decide whether
to do startup pruning.  This avoids changing behavior for existing
cases, but I'm not sure how much it has to recommend it otherwise.
Skipping the startup prune step seems like it'd produce misleading
results in another way, ie showing too many partitions not too few.

This whole business of partition pruning dependent on the
generic parameters is making me uncomfortable.  It seems like
we *can't* show a plan that is either representative of real
execution or comparable to what you get from more-traditional
EXPLAIN usage.  Maybe we need to step back and think more.

            regards, tom lane



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Fri, 2023-02-03 at 15:11 -0500, Tom Lane wrote:
> I can think of a couple of possible ways forward:
>
> * Fix things so that the generic parameters appear to have NULL
> values when inspected during executor startup.  I'm not sure
> how useful that'd be though.  In partition-pruning cases that'd
> lead to EXPLAIN (GENERIC_PLAN) showing the plan with all
> partitions pruned, other than the one for NULL values if there
> is one.  Doesn't sound too helpful.
>
> * Invent another executor flag that's a "stronger" version of
> EXEC_FLAG_EXPLAIN_ONLY, and pass that when any generic parameters
> exist, and check it in CreatePartitionPruneState to decide whether
> to do startup pruning.  This avoids changing behavior for existing
> cases, but I'm not sure how much it has to recommend it otherwise.
> Skipping the startup prune step seems like it'd produce misleading
> results in another way, ie showing too many partitions not too few.
>
> This whole business of partition pruning dependent on the
> generic parameters is making me uncomfortable.  It seems like
> we *can't* show a plan that is either representative of real
> execution or comparable to what you get from more-traditional
> EXPLAIN usage.  Maybe we need to step back and think more.

I slept over it, and the second idea now looks like the the right
approach to me.  My idea of seeing a generic plan is that plan-time
partition pruning happens, but not execution-time pruning, so that
I get no "subplans removed".
Are there any weird side effects of skipping the startup prune step?

Anyway, attached is v7 that tries to do it that way.  It feels fairly
good to me.  I invented a new executor flag EXEC_FLAG_EXPLAIN_GENERIC.
To avoid having to change all the places that check EXEC_FLAG_EXPLAIN_ONLY
to also check for the new flag, I decided that the new flag can only be
used as "add-on" to EXEC_FLAG_EXPLAIN_ONLY.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Andres Freund
Date:
Hi,

On 2023-02-05 18:24:03 +0100, Laurenz Albe wrote:
> Anyway, attached is v7 that tries to do it that way.

This consistently fails on CI:
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest%2F42%2F3962

https://api.cirrus-ci.com/v1/artifact/task/5156723929907200/testrun/build/testrun/regress/regress/regression.diffs

Greetings,

Andres Freund



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Mon, 2023-02-13 at 16:33 -0800, Andres Freund wrote:
> On 2023-02-05 18:24:03 +0100, Laurenz Albe wrote:
> > Anyway, attached is v7 that tries to do it that way.
>
> This consistently fails on CI:
> https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest%2F42%2F3962
>
> https://api.cirrus-ci.com/v1/artifact/task/5156723929907200/testrun/build/testrun/regress/regress/regression.diffs

Thanks for pointing out.

Here is an improved version.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Christoph Berg
Date:
Hi,

I reviewed the patch and find the idea of allowing $placeholders with
EXPLAIN very useful, it will solve relevant real-world use-cases.
(Queries from pg_stat_statements, found in the log, or in application
code.)

I have some comments:

> This allows EXPLAIN to generate generic plans for parameterized statements
> (that have parameter placeholders like $1 in the statement text).

> +   <varlistentry>
> +    <term><literal>GENERIC_PLAN</literal></term>
> +    <listitem>
> +     <para>
> +      Generate a generic plan for the statement (see <xref linkend="sql-prepare"/>
> +      for details about generic plans).  The statement can contain parameter
> +      placeholders like <literal>$1</literal> (but then it has to be a statement
> +      that supports parameters).  This option cannot be used together with
> +      <literal>ANALYZE</literal>, since a statement with unknown parameters
> +      cannot be executed.

Like in the commit message quoted above, I would put more emphasis on
"parameterized query" here:

  Allow the statement to contain parameter placeholders like
  <literal>$1</literal> and generate a generic plan for it.
  This option cannot be used together with <literal>ANALYZE</literal>.

> +    /* check that GENERIC_PLAN is not used with EXPLAIN ANALYZE */
> +    if (es->generic && es->analyze)
> +        ereport(ERROR,
> +                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> +                 errmsg("EXPLAIN ANALYZE cannot be used with GENERIC_PLAN")));

To put that in line with the other error messages in that context, I'd
inject an extra "option":

  errmsg("EXPLAIN option ANALYZE cannot be used with GENERIC_PLAN")));

> --- a/src/test/regress/sql/explain.sql
> +++ b/src/test/regress/sql/explain.sql
> @@ -128,3 +128,33 @@ 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');
> +
> +-- Test EXPLAIN (GENERIC_PLAN)
> +select explain_filter('explain (generic_plan) select unique1 from tenk1 where thousand = $1');
> +-- should fail
> +select explain_filter('explain (analyze, generic_plan) select unique1 from tenk1 where thousand = $1');
> +-- Test EXPLAIN (GENERIC_PLAN) with partition pruning
> +-- should prune at plan time, but not at execution time
> +create extension if not exists postgres_fdw;

"create extension postgres_fdw" cannot be used from src/test/regress/
since contrib/ might not have been built.

> +create server loop42 foreign data wrapper postgres_fdw;
> +create user mapping for current_role server loop42 options (password_required 'false');
> +create table gen_part (
> +  key1 integer not null,
> +  key2 integer not null
> +) partition by list (key1);
> +create table gen_part_1
> +  partition of gen_part for values in (1)
> +  partition by range (key2);
> +create foreign table gen_part_1_1
> +  partition of gen_part_1 for values from (1) to (2)
> +  server loop42 options (table_name 'whatever_1_1');
> +create foreign table gen_part_1_2
> +  partition of gen_part_1 for values from (2) to (3)
> +  server loop42 options (table_name 'whatever_1_2');
> +create foreign table gen_part_2
> +  partition of gen_part for values in (2)
> +  server loop42 options (table_name 'whatever_2');
> +select explain_filter('explain (generic_plan) select key1, key2 from gen_part where key1 = 1 and key2 = $1');

I suggest leaving this test in place here, but with local tables (to
show that plan time pruning using the one provided parameter works),
and add a comment here explaining that is being tested:

-- create a partition hierarchy to show that plan time pruning removes
-- the key1=2 table but generates a generic plan for key2=$1

The test involving postgres_fdw is still necessary to exercise the new
EXEC_FLAG_EXPLAIN_GENERIC code path, but needs to be moved elsewhere,
probably src/test/modules/.

In the new location, likewise add a comment why this test needs to
look this way.

Christoph



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
Thanks for the review!

On Tue, 2023-03-21 at 16:32 +0100, Christoph Berg wrote:
> I have some comments:
>
> > This allows EXPLAIN to generate generic plans for parameterized statements
> > (that have parameter placeholders like $1 in the statement text).
>
> > +   <varlistentry>
> > +    <term><literal>GENERIC_PLAN</literal></term>
> > +    <listitem>
> > +     <para>
> > +      Generate a generic plan for the statement (see <xref linkend="sql-prepare"/>
> > +      for details about generic plans).  The statement can contain parameter
> > +      placeholders like <literal>$1</literal> (but then it has to be a statement
> > +      that supports parameters).  This option cannot be used together with
> > +      <literal>ANALYZE</literal>, since a statement with unknown parameters
> > +      cannot be executed.
>
> Like in the commit message quoted above, I would put more emphasis on
> "parameterized query" here:
>
>   Allow the statement to contain parameter placeholders like
>   <literal>$1</literal> and generate a generic plan for it.
>   This option cannot be used together with <literal>ANALYZE</literal>.

I went with

   Allow the statement to contain parameter placeholders like
   <literal>$1</literal> and generate a generic plan for it.
   See <xref linkend="sql-prepare"/> for details about generic plans
   and the statements that support parameters.
   This option cannot be used together with <literal>ANALYZE</literal>.

> > +       /* check that GENERIC_PLAN is not used with EXPLAIN ANALYZE */
> > +       if (es->generic && es->analyze)
> > +               ereport(ERROR,
> > +                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> > +                                errmsg("EXPLAIN ANALYZE cannot be used with GENERIC_PLAN")));
>
> To put that in line with the other error messages in that context, I'd
> inject an extra "option":
>
>   errmsg("EXPLAIN option ANALYZE cannot be used with GENERIC_PLAN")));

Done.

> > --- a/src/test/regress/sql/explain.sql
> > +++ b/src/test/regress/sql/explain.sql
> > [...]
> > +create extension if not exists postgres_fdw;
>
> "create extension postgres_fdw" cannot be used from src/test/regress/
> since contrib/ might not have been built.

Ouch.  Good catch.

> I suggest leaving this test in place here, but with local tables (to
> show that plan time pruning using the one provided parameter works),
> and add a comment here explaining that is being tested:
>
> -- create a partition hierarchy to show that plan time pruning removes
> -- the key1=2 table but generates a generic plan for key2=$1

I did that, with a different comment.

> The test involving postgres_fdw is still necessary to exercise the new
> EXEC_FLAG_EXPLAIN_GENERIC code path, but needs to be moved elsewhere,
> probably src/test/modules/.

Tests for postgres_fdw are in contrib/postgres_fdw/sql/postgres_fdw.sql,
so I added the test there.

Version 9 of the patch is attached.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
And here is v10, which includes tab completion for the new option.

Yours,
Laurenz Albe

Attachment

Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Christoph Berg
Date:
Re: Laurenz Albe
> And here is v10, which includes tab completion for the new option.

IMHO everything looks good now. Marking as ready for committer.

Thanks!

Christoph



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Tom Lane
Date:
Laurenz Albe <laurenz.albe@cybertec.at> writes:
> On Tue, 2023-03-21 at 16:32 +0100, Christoph Berg wrote:
>> The test involving postgres_fdw is still necessary to exercise the new
>> EXEC_FLAG_EXPLAIN_GENERIC code path, but needs to be moved elsewhere,
>> probably src/test/modules/.

> Tests for postgres_fdw are in contrib/postgres_fdw/sql/postgres_fdw.sql,
> so I added the test there.

I don't actually see why a postgres_fdw test case is needed at all?
The tests in explain.sql seem sufficient.

            regards, tom lane



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Christoph Berg
Date:
Re: Tom Lane
> I don't actually see why a postgres_fdw test case is needed at all?
> The tests in explain.sql seem sufficient.

When I asked Laurenz about that he told me that local tables wouldn't
exercise the code specific for EXEC_FLAG_EXPLAIN_GENERIC.

(Admittedly my knowledge of the planner wasn't deep enough to verify
that. Laurenz is currently traveling, so I don't know if he could
answer this himself now.)

Christoph



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Tom Lane
Date:
Christoph Berg <myon@debian.org> writes:
> Re: Tom Lane
>> I don't actually see why a postgres_fdw test case is needed at all?
>> The tests in explain.sql seem sufficient.

> When I asked Laurenz about that he told me that local tables wouldn't
> exercise the code specific for EXEC_FLAG_EXPLAIN_GENERIC.

But there isn't any ... or at least, I fail to see what isn't sufficiently
exercised by that new explain.sql test case that's identical to this one
except for being a non-foreign table.  Perhaps at some point this patch
modified postgres_fdw code?  But it doesn't now.

I don't mind having a postgres_fdw test if there's something for it
to test, but it just looks duplicative to me.  Other things being
equal, I'd prefer to test this feature in explain.sql, since (a) it's
a core feature and (b) the core tests are better parallelized than the
contrib tests, so the same test should be cheaper to run.

> (Admittedly my knowledge of the planner wasn't deep enough to verify
> that. Laurenz is currently traveling, so I don't know if he could
> answer this himself now.)

OK, thanks for the status update.  What I'll do to get this off my
plate is to push the patch without the postgres_fdw test -- if
Laurenz wants to advocate for that when he returns, we can discuss it
more.

            regards, tom lane



Re: Make EXPLAIN generate a generic plan for a parameterized query

From
Laurenz Albe
Date:
On Fri, 2023-03-24 at 16:41 -0400, Tom Lane wrote:
> Christoph Berg <myon@debian.org> writes:
> > Re: Tom Lane
> > > I don't actually see why a postgres_fdw test case is needed at all?
> > > The tests in explain.sql seem sufficient.
>
> > When I asked Laurenz about that he told me that local tables wouldn't
> > exercise the code specific for EXEC_FLAG_EXPLAIN_GENERIC.
>
> But there isn't any ... or at least, I fail to see what isn't sufficiently
> exercised by that new explain.sql test case that's identical to this one
> except for being a non-foreign table.  Perhaps at some point this patch
> modified postgres_fdw code?  But it doesn't now.
>
> I don't mind having a postgres_fdw test if there's something for it
> to test, but it just looks duplicative to me.  Other things being
> equal, I'd prefer to test this feature in explain.sql, since (a) it's
> a core feature and (b) the core tests are better parallelized than the
> contrib tests, so the same test should be cheaper to run.
>
> > (Admittedly my knowledge of the planner wasn't deep enough to verify
> > that. Laurenz is currently traveling, so I don't know if he could
> > answer this himself now.)
>
> OK, thanks for the status update.  What I'll do to get this off my
> plate is to push the patch without the postgres_fdw test -- if
> Laurenz wants to advocate for that when he returns, we can discuss it
> more.

Thanks for committing this.

As Chrisoph mentioned, I found that I could not reproduce the problem
that was addressed by the EXEC_FLAG_EXPLAIN_GENERIC hack using local
partitioned tables.  My optimizer knowledge is not deep enough to tell
why, and it might well be a bug lurking in the FDW part of the
optimizer code.  It is not FDW specific, since I discovered it with
oracle_fdw and could reproduce it with postgres_fdw.

I was aware that it is awkward to add a test to a contrib module, but
I thought that I should add a test that exercises the new code path.
But I am fine without the postgres_fdw test.

Yours,
Laurenz Albe