Skip to content

Commit 983ef36

Browse files
atoriklfittl
authored andcommitted
Add function to log the plan of the currently running
Currently, we have to wait for the query execution to finish to know its plan either using EXPLAIN ANALYZE or auto_explain. This is not so convenient, for example when investigating long-running queries on production environments. To improve this situation, this patch adds pg_log_query_plan() function that requests to log the plan of the currently executing query. On receipt of the request, ExecProcNodes of the current plan node and its subsidiary nodes are wrapped with ExecProcNodeFirst, which implelements logging query plan. When executor executes the one of the wrapped nodes, the query plan is logged. Our initial idea was to send a signal to the target backend process, which invokes EXPLAIN logic at the next CHECK_FOR_INTERRUPTS() call. However, we realized during prototyping that EXPLAIN is complex and may not be safely executed at arbitrary interrupt points. By default, only superusers are allowed to request to log the plans because allowing any users to issue this request at an unbounded rate would cause lots of log messages and which can lead to denial of service. Todo: Consider removing the PID from the log output of pg_log_backend_memory_contexts() and pg_log_query_plan(). For detail, see the discussion: https://www.postgresql.org/message-id/CA%2BTgmoY81r7npTS34N_5MLA_u6ghfor5HoSaar53veXUYu1OxQ%40mail.gmail.com
1 parent 89eafad commit 983ef36

24 files changed

Lines changed: 652 additions & 40 deletions

File tree

contrib/auto_explain/auto_explain.c

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include "access/parallel.h"
1818
#include "commands/defrem.h"
19+
#include "commands/dynamic_explain.h"
1920
#include "commands/explain.h"
2021
#include "commands/explain_format.h"
2122
#include "commands/explain_state.h"
@@ -451,32 +452,9 @@ explain_ExecutorEnd(QueryDesc *queryDesc)
451452

452453
apply_extension_options(es, extension_options);
453454

454-
ExplainBeginOutput(es);
455-
ExplainQueryText(es, queryDesc);
456-
ExplainQueryParameters(es, queryDesc->params, auto_explain_log_parameter_max_length);
457-
ExplainPrintPlan(es, queryDesc);
458-
if (es->analyze && auto_explain_log_triggers)
459-
ExplainPrintTriggers(es, queryDesc);
460-
if (es->costs)
461-
ExplainPrintJITSummary(es, queryDesc);
462-
if (explain_per_plan_hook)
463-
(*explain_per_plan_hook) (queryDesc->plannedstmt,
464-
NULL, es,
465-
queryDesc->sourceText,
466-
queryDesc->params,
467-
queryDesc->estate->es_queryEnv);
468-
ExplainEndOutput(es);
469-
470-
/* Remove last line break */
471-
if (es->str->len > 0 && es->str->data[es->str->len - 1] == '\n')
472-
es->str->data[--es->str->len] = '\0';
473-
474-
/* Fix JSON to output an object */
475-
if (auto_explain_log_format == EXPLAIN_FORMAT_JSON)
476-
{
477-
es->str->data[0] = '{';
478-
es->str->data[es->str->len - 1] = '}';
479-
}
455+
ExplainStringAssemble(es, queryDesc, auto_explain_log_format,
456+
auto_explain_log_triggers,
457+
auto_explain_log_parameter_max_length);
480458

481459
/*
482460
* Note: we rely on the existing logging of context or

doc/src/sgml/func/func-admin.sgml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,30 @@
184184
</para></entry>
185185
</row>
186186

187+
<row>
188+
<entry role="func_table_entry"><para role="func_signature">
189+
<indexterm>
190+
<primary>pg_log_query_plan</primary>
191+
</indexterm>
192+
<function>pg_log_query_plan</function> ( <parameter>pid</parameter> <type>integer</type> )
193+
<returnvalue>boolean</returnvalue>
194+
</para>
195+
<para>
196+
Requests to log the plan of the query currently running on the
197+
backend with specified process ID.
198+
It will be logged at <literal>LOG</literal> message level and
199+
will appear in the server log based on the log
200+
configuration set (See <xref linkend="runtime-config-logging"/>
201+
for more information), but will not be sent to the client
202+
regardless of <xref linkend="guc-client-min-messages"/>.
203+
</para>
204+
<para>
205+
This function is restricted to superusers by default, but other
206+
users can be granted EXECUTE to run the function.
207+
</para></entry>
208+
</row>
209+
210+
187211
<row>
188212
<entry role="func_table_entry"><para role="func_signature">
189213
<indexterm>

src/backend/access/transam/xact.c

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "catalog/pg_enum.h"
3838
#include "catalog/storage.h"
3939
#include "commands/async.h"
40+
#include "commands/dynamic_explain.h"
4041
#include "commands/tablecmds.h"
4142
#include "commands/trigger.h"
4243
#include "common/pg_prng.h"
@@ -217,6 +218,7 @@ typedef struct TransactionStateData
217218
bool parallelChildXact; /* is any parent transaction parallel? */
218219
bool chain; /* start a new block after this one */
219220
bool topXidLogged; /* for a subxact: is top-level XID logged? */
221+
QueryDesc *queryDesc; /* my current QueryDesc */
220222
struct TransactionStateData *parent; /* back link to parent */
221223
} TransactionStateData;
222224

@@ -250,6 +252,7 @@ static TransactionStateData TopTransactionStateData = {
250252
.state = TRANS_DEFAULT,
251253
.blockState = TBLOCK_DEFAULT,
252254
.topXidLogged = false,
255+
.queryDesc = NULL,
253256
};
254257

255258
/*
@@ -935,6 +938,27 @@ GetCurrentTransactionNestLevel(void)
935938
return s->nestingLevel;
936939
}
937940

941+
/*
942+
* SetCurrentQueryDesc
943+
*/
944+
void
945+
SetCurrentQueryDesc(QueryDesc *queryDesc)
946+
{
947+
TransactionState s = CurrentTransactionState;
948+
949+
s->queryDesc = queryDesc;
950+
}
951+
952+
/*
953+
* GetCurrentQueryDesc
954+
*/
955+
QueryDesc *
956+
GetCurrentQueryDesc(void)
957+
{
958+
TransactionState s = CurrentTransactionState;
959+
960+
return s->queryDesc;
961+
}
938962

939963
/*
940964
* TransactionIdIsCurrentTransactionId
@@ -2953,6 +2977,9 @@ AbortTransaction(void)
29532977
/* Reset snapshot export state. */
29542978
SnapBuildResetExportedSnapshotState();
29552979

2980+
/* Reset current query plan state used for logging. */
2981+
SetCurrentQueryDesc(NULL);
2982+
29562983
/*
29572984
* If this xact has started any unfinished parallel operation, clean up
29582985
* its workers and exit parallel mode. Don't warn about leaked resources.
@@ -5352,6 +5379,13 @@ AbortSubTransaction(void)
53525379
/* Reset logical streaming state. */
53535380
ResetLogicalStreamingState();
53545381

5382+
/*
5383+
* Reset current query plan state used for logging. Note that even after
5384+
* this reset, it's still possible to obtain the parent transaction's
5385+
* query plans, since they are preserved in standard_ExecutorRun().
5386+
*/
5387+
SetCurrentQueryDesc(NULL);
5388+
53555389
/*
53565390
* No need for SnapBuildResetExportedSnapshotState() here, snapshot
53575391
* exports are not supported in subtransactions.

src/backend/commands/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ OBJS = \
3131
define.o \
3232
discard.o \
3333
dropcmds.o \
34+
dynamic_explain.o \
3435
event_trigger.o \
3536
explain.o \
3637
explain_dr.o \
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*-------------------------------------------------------------------------
2+
*
3+
* dynamic_explain.c
4+
* Explain query plans during execution
5+
*
6+
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
7+
* Portions Copyright (c) 1994-5, Regents of the University of California
8+
*
9+
* IDENTIFICATION
10+
* src/backend/commands/dynamic_explain.c
11+
*
12+
*-------------------------------------------------------------------------
13+
*/
14+
#include "postgres.h"
15+
16+
#include "access/xact.h"
17+
#include "commands/dynamic_explain.h"
18+
#include "commands/explain.h"
19+
#include "commands/explain_format.h"
20+
#include "commands/explain_state.h"
21+
#include "miscadmin.h"
22+
#include "storage/proc.h"
23+
#include "storage/procarray.h"
24+
#include "storage/procsignal.h"
25+
#include "utils/backend_status.h"
26+
#include "utils/injection_point.h"
27+
28+
/* Is plan node wrapping for query plan logging currently in progress? */
29+
static bool WrapNodesInProgress = false;
30+
31+
/*
32+
* Handle receipt of an interrupt indicating logging the plan of the currently
33+
* running query.
34+
*
35+
* All the actual work is deferred to ProcessLogQueryPlanInterrupt(),
36+
* because we cannot safely emit a log message inside the signal handler.
37+
*/
38+
void
39+
HandleLogQueryPlanInterrupt(void)
40+
{
41+
#ifdef USE_INJECTION_POINTS
42+
INJECTION_POINT("log-query-interrupt", NULL);
43+
#endif
44+
InterruptPending = true;
45+
LogQueryPlanPending = true;
46+
/* latch will be set by procsignal_sigusr1_handler */
47+
}
48+
49+
/*
50+
* Actual plan logging function.
51+
*/
52+
void
53+
LogQueryPlan(void)
54+
{
55+
ExplainState *es;
56+
MemoryContext cxt;
57+
MemoryContext old_cxt;
58+
QueryDesc *queryDesc;
59+
60+
cxt = AllocSetContextCreate(CurrentMemoryContext,
61+
"log_query_plan temporary context",
62+
ALLOCSET_DEFAULT_SIZES);
63+
64+
old_cxt = MemoryContextSwitchTo(cxt);
65+
66+
es = NewExplainState();
67+
68+
es->format = EXPLAIN_FORMAT_TEXT;
69+
es->settings = true;
70+
es->verbose = true;
71+
es->signaled = true;
72+
73+
/*
74+
* Current QueryDesc is valid only during standard_ExecutorRun. However,
75+
* ExecProcNode can be called afterward(i.e., ExecPostprocessPlan). To
76+
* handle the case, check whether we have QueryDesc now.
77+
*/
78+
queryDesc = GetCurrentQueryDesc();
79+
80+
if (queryDesc == NULL)
81+
{
82+
LogQueryPlanPending = false;
83+
return;
84+
}
85+
86+
ExplainStringAssemble(es, queryDesc, es->format, 0, -1);
87+
88+
ereport(LOG_SERVER_ONLY,
89+
errmsg("query and its plan running on backend with PID %d are:\n%s",
90+
MyProcPid, es->str->data));
91+
92+
MemoryContextSwitchTo(old_cxt);
93+
MemoryContextDelete(cxt);
94+
95+
LogQueryPlanPending = false;
96+
}
97+
98+
/*
99+
* Process the request for logging query plan at CHECK_FOR_INTERRUPTS().
100+
*
101+
* Since executing EXPLAIN-related code at an arbitrary CHECK_FOR_INTERRUPTS()
102+
* point is potentially unsafe, this function just wraps the nodes of
103+
* ExecProcNode with ExecProcNodeFirst, which logs query plan if requested.
104+
* This way ensures that EXPLAIN-related code is executed only during
105+
* ExecProcNodeFirst, where it is considered safe.
106+
*/
107+
void
108+
ProcessLogQueryPlanInterrupt(void)
109+
{
110+
QueryDesc *querydesc = GetCurrentQueryDesc();
111+
112+
/* If current query has already finished, we can do nothing but exit */
113+
if (querydesc == NULL)
114+
{
115+
LogQueryPlanPending = false;
116+
return;
117+
}
118+
119+
/*
120+
* Exit immediately if wrapping plan is already in progress. This prevents
121+
* recursive calls, which could occur if logging is requested repeatedly and
122+
* rapidly, potentially leading to infinite recursion and crash.
123+
*/
124+
if (WrapNodesInProgress)
125+
return;
126+
127+
WrapNodesInProgress = true;
128+
129+
PG_TRY();
130+
{
131+
/*
132+
* Wrap ExecProcNodes with ExecProcNodeFirst, which logs query plan
133+
* when LogQueryPlanPending is true.
134+
*/
135+
ExecSetExecProcNodeRecurse(querydesc->planstate);
136+
}
137+
PG_FINALLY();
138+
{
139+
WrapNodesInProgress = false;
140+
}
141+
PG_END_TRY();
142+
}
143+
144+
/*
145+
* Signal a backend process to log the query plan of the running query.
146+
*
147+
* By default, only superusers are allowed to signal to log the plan because
148+
* allowing any users to issue this request at an unbounded rate would
149+
* cause lots of log messages and which can lead to denial of service.
150+
* Additional roles can be permitted with GRANT.
151+
*/
152+
Datum
153+
pg_log_query_plan(PG_FUNCTION_ARGS)
154+
{
155+
int pid = PG_GETARG_INT32(0);
156+
PGPROC *proc;
157+
PgBackendStatus *be_status;
158+
159+
proc = BackendPidGetProc(pid);
160+
161+
if (proc == NULL)
162+
{
163+
/*
164+
* This is just a warning so a loop-through-resultset will not abort
165+
* if one backend terminated on its own during the run.
166+
*/
167+
ereport(WARNING,
168+
(errmsg("PID %d is not a PostgreSQL backend process", pid)));
169+
PG_RETURN_BOOL(false);
170+
}
171+
172+
be_status = pgstat_get_beentry_by_proc_number(proc->vxid.procNumber);
173+
if (be_status->st_backendType != B_BACKEND)
174+
{
175+
ereport(WARNING,
176+
(errmsg("PID %d is not a PostgreSQL client backend process", pid)));
177+
PG_RETURN_BOOL(false);
178+
}
179+
180+
if (SendProcSignal(pid, PROCSIG_LOG_QUERY_PLAN, proc->vxid.procNumber) < 0)
181+
{
182+
ereport(WARNING,
183+
(errmsg("could not send signal to process %d: %m", pid)));
184+
PG_RETURN_BOOL(false);
185+
}
186+
187+
PG_RETURN_BOOL(true);
188+
}

0 commit comments

Comments
 (0)