Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * pg_plan_advice.c
4 : * main entrypoints for generating and applying planner advice
5 : *
6 : * Copyright (c) 2016-2026, PostgreSQL Global Development Group
7 : *
8 : * contrib/pg_plan_advice/pg_plan_advice.c
9 : *
10 : *-------------------------------------------------------------------------
11 : */
12 : #include "postgres.h"
13 :
14 : #include "pg_plan_advice.h"
15 : #include "pgpa_ast.h"
16 : #include "pgpa_identifier.h"
17 : #include "pgpa_output.h"
18 : #include "pgpa_planner.h"
19 : #include "pgpa_trove.h"
20 : #include "pgpa_walker.h"
21 :
22 : #include "commands/defrem.h"
23 : #include "commands/explain.h"
24 : #include "commands/explain_format.h"
25 : #include "commands/explain_state.h"
26 : #include "funcapi.h"
27 : #include "optimizer/planner.h"
28 : #include "storage/dsm_registry.h"
29 : #include "utils/guc.h"
30 :
31 11 : PG_MODULE_MAGIC;
32 :
33 : /* GUC variables */
34 : char *pg_plan_advice_advice = NULL;
35 : bool pg_plan_advice_always_store_advice_details = false;
36 : static bool pg_plan_advice_always_explain_supplied_advice = true;
37 : bool pg_plan_advice_feedback_warnings = false;
38 : bool pg_plan_advice_trace_mask = false;
39 :
40 : /* Saved hook value */
41 : static explain_per_plan_hook_type prev_explain_per_plan = NULL;
42 :
43 : /* Other file-level globals */
44 : static int es_extension_id;
45 : static MemoryContext pgpa_memory_context = NULL;
46 : static List *advisor_hook_list = NIL;
47 :
48 : static void pg_plan_advice_explain_option_handler(ExplainState *es,
49 : DefElem *opt,
50 : ParseState *pstate);
51 : static void pg_plan_advice_explain_per_plan_hook(PlannedStmt *plannedstmt,
52 : IntoClause *into,
53 : ExplainState *es,
54 : const char *queryString,
55 : ParamListInfo params,
56 : QueryEnvironment *queryEnv);
57 : static bool pg_plan_advice_advice_check_hook(char **newval, void **extra,
58 : GucSource source);
59 : static DefElem *find_defelem_by_defname(List *deflist, char *defname);
60 :
61 : /*
62 : * Initialize this module.
63 : */
64 : void
65 11 : _PG_init(void)
66 : {
67 11 : DefineCustomStringVariable("pg_plan_advice.advice",
68 : "advice to apply during query planning",
69 : NULL,
70 : &pg_plan_advice_advice,
71 : NULL,
72 : PGC_USERSET,
73 : 0,
74 : pg_plan_advice_advice_check_hook,
75 : NULL,
76 : NULL);
77 :
78 11 : DefineCustomBoolVariable("pg_plan_advice.always_explain_supplied_advice",
79 : "EXPLAIN output includes supplied advice even without EXPLAIN (PLAN_ADVICE)",
80 : NULL,
81 : &pg_plan_advice_always_explain_supplied_advice,
82 : true,
83 : PGC_USERSET,
84 : 0,
85 : NULL,
86 : NULL,
87 : NULL);
88 :
89 11 : DefineCustomBoolVariable("pg_plan_advice.always_store_advice_details",
90 : "Generate advice strings even when seemingly not required",
91 : "Use this option to see generated advice for prepared queries.",
92 : &pg_plan_advice_always_store_advice_details,
93 : false,
94 : PGC_USERSET,
95 : 0,
96 : NULL,
97 : NULL,
98 : NULL);
99 :
100 11 : DefineCustomBoolVariable("pg_plan_advice.feedback_warnings",
101 : "Warn when supplied advice does not apply cleanly",
102 : NULL,
103 : &pg_plan_advice_feedback_warnings,
104 : false,
105 : PGC_USERSET,
106 : 0,
107 : NULL,
108 : NULL,
109 : NULL);
110 :
111 11 : DefineCustomBoolVariable("pg_plan_advice.trace_mask",
112 : "Emit debugging messages showing the computed strategy mask for each relation",
113 : NULL,
114 : &pg_plan_advice_trace_mask,
115 : false,
116 : PGC_USERSET,
117 : 0,
118 : NULL,
119 : NULL,
120 : NULL);
121 :
122 11 : MarkGUCPrefixReserved("pg_plan_advice");
123 :
124 : /* Get an ID that we can use to cache data in an ExplainState. */
125 11 : es_extension_id = GetExplainExtensionId("pg_plan_advice");
126 :
127 : /* Register the new EXPLAIN options implemented by this module. */
128 11 : RegisterExtensionExplainOption("plan_advice",
129 : pg_plan_advice_explain_option_handler);
130 :
131 : /* Install hooks */
132 11 : pgpa_planner_install_hooks();
133 11 : prev_explain_per_plan = explain_per_plan_hook;
134 11 : explain_per_plan_hook = pg_plan_advice_explain_per_plan_hook;
135 11 : }
136 :
137 : /*
138 : * Return a pointer to a memory context where long-lived data managed by this
139 : * module can be stored.
140 : */
141 : MemoryContext
142 0 : pg_plan_advice_get_mcxt(void)
143 : {
144 0 : if (pgpa_memory_context == NULL)
145 0 : pgpa_memory_context = AllocSetContextCreate(TopMemoryContext,
146 : "pg_plan_advice",
147 : ALLOCSET_DEFAULT_SIZES);
148 :
149 0 : return pgpa_memory_context;
150 : }
151 :
152 : /*
153 : * Was the PLAN_ADVICE option specified and not set to false?
154 : */
155 : bool
156 311 : pg_plan_advice_should_explain(ExplainState *es)
157 : {
158 311 : bool *plan_advice = NULL;
159 :
160 311 : if (es != NULL)
161 272 : plan_advice = GetExplainExtensionState(es, es_extension_id);
162 311 : return plan_advice != NULL && *plan_advice;
163 : }
164 :
165 : /*
166 : * Get the advice that should be used while planning a particular query.
167 : */
168 : char *
169 174 : pg_plan_advice_get_supplied_query_advice(PlannerGlobal *glob,
170 : Query *parse,
171 : const char *query_string,
172 : int cursorOptions,
173 : ExplainState *es)
174 : {
175 : ListCell *lc;
176 :
177 : /*
178 : * If any advisors are loaded, consult them. The first one that produces a
179 : * non-NULL string wins.
180 : */
181 174 : foreach(lc, advisor_hook_list)
182 : {
183 0 : pg_plan_advice_advisor_hook hook = lfirst(lc);
184 : char *advice_string;
185 :
186 0 : advice_string = (*hook) (glob, parse, query_string, cursorOptions, es);
187 0 : if (advice_string != NULL)
188 0 : return advice_string;
189 : }
190 :
191 : /* Otherwise, just use the value of the GUC. */
192 174 : return pg_plan_advice_advice;
193 : }
194 :
195 : /*
196 : * Add an advisor, which can supply advice strings to be used during future
197 : * query planning operations.
198 : *
199 : * The advisor should return NULL if it has no advice string to offer for a
200 : * given query. If multiple advisors are added, they will be consulted in the
201 : * order added until one of them returns a non-NULL value.
202 : */
203 : void
204 0 : pg_plan_advice_add_advisor(pg_plan_advice_advisor_hook hook)
205 : {
206 : MemoryContext oldcontext;
207 :
208 0 : oldcontext = MemoryContextSwitchTo(pg_plan_advice_get_mcxt());
209 0 : advisor_hook_list = lappend(advisor_hook_list, hook);
210 0 : MemoryContextSwitchTo(oldcontext);
211 0 : }
212 :
213 : /*
214 : * Remove an advisor.
215 : */
216 : void
217 0 : pg_plan_advice_remove_advisor(pg_plan_advice_advisor_hook hook)
218 : {
219 : MemoryContext oldcontext;
220 :
221 0 : oldcontext = MemoryContextSwitchTo(pg_plan_advice_get_mcxt());
222 0 : advisor_hook_list = list_delete_ptr(advisor_hook_list, hook);
223 0 : MemoryContextSwitchTo(oldcontext);
224 0 : }
225 :
226 : /*
227 : * Other loadable modules can use this function to trigger advice generation.
228 : *
229 : * Calling this function with activate = true requests that any queries
230 : * planned afterwards should generate plan advice, which will be stored in the
231 : * PlannedStmt. Calling this function with activate = false revokes that
232 : * request. Multiple loadable modules could be using this simultaneously, so
233 : * make sure to only revoke your own requests.
234 : *
235 : * Note that you can't use this function to *suppress* advice generation,
236 : * which can occur for other reasons, such as the use of EXPLAIN (PLAN_ADVICE),
237 : * regardless. It's a way of turning advice generation on, not a way of turning
238 : * it off.
239 : */
240 : void
241 0 : pg_plan_advice_request_advice_generation(bool activate)
242 : {
243 0 : if (activate)
244 0 : pgpa_planner_generate_advice++;
245 : else
246 : {
247 : Assert(pgpa_planner_generate_advice > 0);
248 0 : pgpa_planner_generate_advice--;
249 : }
250 0 : }
251 :
252 : /*
253 : * Handler for EXPLAIN (PLAN_ADVICE).
254 : */
255 : static void
256 122 : pg_plan_advice_explain_option_handler(ExplainState *es, DefElem *opt,
257 : ParseState *pstate)
258 : {
259 : bool *plan_advice;
260 :
261 122 : plan_advice = GetExplainExtensionState(es, es_extension_id);
262 :
263 122 : if (plan_advice == NULL)
264 : {
265 122 : plan_advice = palloc0_object(bool);
266 122 : SetExplainExtensionState(es, es_extension_id, plan_advice);
267 : }
268 :
269 122 : *plan_advice = defGetBoolean(opt);
270 122 : }
271 :
272 : /*
273 : * Display a string that is likely to consist of multiple lines in EXPLAIN
274 : * output.
275 : */
276 : static void
277 234 : pg_plan_advice_explain_text_multiline(ExplainState *es, char *qlabel,
278 : char *value)
279 : {
280 : char *s;
281 :
282 : /* For non-text formats, it's best not to add any special handling. */
283 234 : if (es->format != EXPLAIN_FORMAT_TEXT)
284 : {
285 1 : ExplainPropertyText(qlabel, value, es);
286 1 : return;
287 : }
288 :
289 : /* In text format, if there is no data, display nothing. */
290 233 : if (*value == '\0')
291 1 : return;
292 :
293 : /*
294 : * It looks nicest to indent each line of the advice separately, beginning
295 : * on the line below the label.
296 : */
297 232 : ExplainIndentText(es);
298 232 : appendStringInfo(es->str, "%s:\n", qlabel);
299 232 : es->indent++;
300 723 : while ((s = strchr(value, '\n')) != NULL)
301 : {
302 491 : ExplainIndentText(es);
303 491 : appendBinaryStringInfo(es->str, value, (s - value) + 1);
304 491 : value = s + 1;
305 : }
306 :
307 : /* Don't interpret a terminal newline as a request for an empty line. */
308 232 : if (*value != '\0')
309 : {
310 121 : ExplainIndentText(es);
311 121 : appendStringInfo(es->str, "%s\n", value);
312 : }
313 :
314 232 : es->indent--;
315 : }
316 :
317 : /*
318 : * Add advice feedback to the EXPLAIN output.
319 : */
320 : static void
321 113 : pg_plan_advice_explain_feedback(ExplainState *es, List *feedback)
322 : {
323 : StringInfoData buf;
324 :
325 113 : initStringInfo(&buf);
326 359 : foreach_node(DefElem, item, feedback)
327 : {
328 133 : int flags = defGetInt32(item);
329 :
330 133 : appendStringInfo(&buf, "%s /* ", item->defname);
331 133 : pgpa_trove_append_flags(&buf, flags);
332 133 : appendStringInfo(&buf, " */\n");
333 : }
334 :
335 113 : pg_plan_advice_explain_text_multiline(es, "Supplied Plan Advice",
336 : buf.data);
337 113 : }
338 :
339 : /*
340 : * Add relevant details, if any, to the EXPLAIN output for a single plan.
341 : */
342 : static void
343 138 : pg_plan_advice_explain_per_plan_hook(PlannedStmt *plannedstmt,
344 : IntoClause *into,
345 : ExplainState *es,
346 : const char *queryString,
347 : ParamListInfo params,
348 : QueryEnvironment *queryEnv)
349 : {
350 : bool should_explain;
351 : DefElem *pgpa_item;
352 : List *pgpa_list;
353 :
354 138 : if (prev_explain_per_plan)
355 0 : prev_explain_per_plan(plannedstmt, into, es, queryString, params,
356 : queryEnv);
357 :
358 : /* Should an advice string be part of the EXPLAIN output? */
359 138 : should_explain = pg_plan_advice_should_explain(es);
360 :
361 : /* Find any data pgpa_planner_shutdown stashed in the PlannedStmt. */
362 138 : pgpa_item = find_defelem_by_defname(plannedstmt->extension_state,
363 : "pg_plan_advice");
364 138 : pgpa_list = pgpa_item == NULL ? NULL : (List *) pgpa_item->arg;
365 :
366 : /*
367 : * By default, if there is a record of attempting to apply advice during
368 : * query planning, we always output that information, but the user can set
369 : * pg_plan_advice.always_explain_supplied_advice = false to suppress that
370 : * behavior. If they do, we'll only display it when the PLAN_ADVICE option
371 : * was specified and not set to false.
372 : *
373 : * NB: If we're explaining a query planned beforehand -- i.e. a prepared
374 : * statement -- the application of query advice may not have been
375 : * recorded, and therefore this won't be able to show anything. Use
376 : * pg_plan_advice.always_store_advice_details = true to work around this.
377 : */
378 138 : if (pgpa_list != NULL && (pg_plan_advice_always_explain_supplied_advice ||
379 : should_explain))
380 : {
381 : DefElem *feedback;
382 :
383 133 : feedback = find_defelem_by_defname(pgpa_list, "feedback");
384 133 : if (feedback != NULL)
385 113 : pg_plan_advice_explain_feedback(es, (List *) feedback->arg);
386 : }
387 :
388 : /*
389 : * If the PLAN_ADVICE option was specified -- and not set to FALSE -- show
390 : * generated advice.
391 : */
392 138 : if (should_explain)
393 : {
394 : DefElem *advice_string_item;
395 122 : char *advice_string = NULL;
396 :
397 : advice_string_item =
398 122 : find_defelem_by_defname(pgpa_list, "advice_string");
399 122 : if (advice_string_item != NULL)
400 : {
401 121 : advice_string = strVal(advice_string_item->arg);
402 121 : pg_plan_advice_explain_text_multiline(es, "Generated Plan Advice",
403 : advice_string);
404 : }
405 : }
406 138 : }
407 :
408 : /*
409 : * Check hook for pg_plan_advice.advice
410 : */
411 : static bool
412 139 : pg_plan_advice_advice_check_hook(char **newval, void **extra, GucSource source)
413 : {
414 : MemoryContext oldcontext;
415 : MemoryContext tmpcontext;
416 : char *error;
417 :
418 139 : if (*newval == NULL)
419 11 : return true;
420 :
421 128 : tmpcontext = AllocSetContextCreate(CurrentMemoryContext,
422 : "pg_plan_advice.advice",
423 : ALLOCSET_DEFAULT_SIZES);
424 128 : oldcontext = MemoryContextSwitchTo(tmpcontext);
425 :
426 : /*
427 : * It would be nice to save the parse tree that we construct here for
428 : * eventual use when planning with this advice, but *extra can only point
429 : * to a single guc_malloc'd chunk, and our parse tree involves an
430 : * arbitrary number of memory allocations.
431 : */
432 128 : (void) pgpa_parse(*newval, &error);
433 :
434 128 : if (error != NULL)
435 17 : GUC_check_errdetail("Could not parse advice: %s", error);
436 :
437 128 : MemoryContextSwitchTo(oldcontext);
438 128 : MemoryContextDelete(tmpcontext);
439 :
440 128 : return (error == NULL);
441 : }
442 :
443 : /*
444 : * Search a list of DefElem objects for a given defname.
445 : */
446 : static DefElem *
447 393 : find_defelem_by_defname(List *deflist, char *defname)
448 : {
449 540 : foreach_node(DefElem, item, deflist)
450 : {
451 488 : if (strcmp(item->defname, defname) == 0)
452 367 : return item;
453 : }
454 :
455 26 : return NULL;
456 : }
|