Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * pgpa_output.c
4 : * produce textual output from the results of a plan tree walk
5 : *
6 : * Copyright (c) 2016-2026, PostgreSQL Global Development Group
7 : *
8 : * contrib/pg_plan_advice/pgpa_output.c
9 : *
10 : *-------------------------------------------------------------------------
11 : */
12 :
13 : #include "postgres.h"
14 :
15 : #include "pgpa_output.h"
16 : #include "pgpa_scan.h"
17 :
18 : #include "nodes/parsenodes.h"
19 : #include "parser/parsetree.h"
20 : #include "utils/builtins.h"
21 : #include "utils/lsyscache.h"
22 :
23 : /*
24 : * Context object for textual advice generation.
25 : *
26 : * rt_identifiers is the caller-provided array of range table identifiers.
27 : * See the comments at the top of pgpa_identifier.c for more details.
28 : *
29 : * buf is the caller-provided output buffer.
30 : *
31 : * wrap_column is the wrap column, so that we don't create output that is
32 : * too wide. See pgpa_maybe_linebreak() and comments in pgpa_output_advice.
33 : */
34 : typedef struct pgpa_output_context
35 : {
36 : const char **rid_strings;
37 : StringInfo buf;
38 : int wrap_column;
39 : } pgpa_output_context;
40 :
41 : static void pgpa_output_unrolled_join(pgpa_output_context *context,
42 : pgpa_unrolled_join *join);
43 : static void pgpa_output_join_member(pgpa_output_context *context,
44 : pgpa_join_member *member);
45 : static void pgpa_output_scan_strategy(pgpa_output_context *context,
46 : pgpa_scan_strategy strategy,
47 : List *scans);
48 : static void pgpa_output_relation_name(pgpa_output_context *context, Oid relid);
49 : static void pgpa_output_query_feature(pgpa_output_context *context,
50 : pgpa_qf_type type,
51 : List *query_features);
52 : static void pgpa_output_simple_strategy(pgpa_output_context *context,
53 : char *strategy,
54 : List *relid_sets);
55 : static void pgpa_output_no_gather(pgpa_output_context *context,
56 : Bitmapset *relids);
57 : static void pgpa_output_relations(pgpa_output_context *context, StringInfo buf,
58 : Bitmapset *relids);
59 :
60 : static char *pgpa_cstring_join_strategy(pgpa_join_strategy strategy);
61 : static char *pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy);
62 : static char *pgpa_cstring_query_feature_type(pgpa_qf_type type);
63 :
64 : static void pgpa_maybe_linebreak(StringInfo buf, int wrap_column);
65 :
66 : /*
67 : * Append query advice to the provided buffer.
68 : *
69 : * Before calling this function, 'walker' must be used to iterate over the
70 : * main plan tree and all subplans from the PlannedStmt.
71 : *
72 : * 'rt_identifiers' is a table of unique identifiers, one for each RTI.
73 : * See pgpa_create_identifiers_for_planned_stmt().
74 : *
75 : * Results will be appended to 'buf'.
76 : */
77 : void
78 120 : pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
79 : pgpa_identifier *rt_identifiers)
80 : {
81 120 : Index rtable_length = list_length(walker->pstmt->rtable);
82 : ListCell *lc;
83 : pgpa_output_context context;
84 :
85 : /* Basic initialization. */
86 120 : memset(&context, 0, sizeof(pgpa_output_context));
87 120 : context.buf = buf;
88 :
89 : /*
90 : * Convert identifiers to string form. Note that the loop variable here is
91 : * not an RTI, because RTIs are 1-based. Some RTIs will have no
92 : * identifier, either because the reloptkind is RTE_JOIN or because that
93 : * portion of the query didn't make it into the final plan.
94 : */
95 120 : context.rid_strings = palloc0_array(const char *, rtable_length);
96 489 : for (int i = 0; i < rtable_length; ++i)
97 369 : if (rt_identifiers[i].alias_name != NULL)
98 299 : context.rid_strings[i] = pgpa_identifier_string(&rt_identifiers[i]);
99 :
100 : /*
101 : * If the user chooses to use EXPLAIN (PLAN_ADVICE) in an 80-column window
102 : * from a psql client with default settings, psql will add one space to
103 : * the left of the output and EXPLAIN will add two more to the left of the
104 : * advice. Thus, lines of more than 77 characters will wrap. We set the
105 : * wrap limit to 76 here so that the output won't reach all the way to the
106 : * very last column of the terminal.
107 : *
108 : * Of course, this is fairly arbitrary set of assumptions, and one could
109 : * well make an argument for a different wrap limit, or for a configurable
110 : * one.
111 : */
112 120 : context.wrap_column = 76;
113 :
114 : /*
115 : * Each piece of JOIN_ORDER() advice fully describes the join order for a
116 : * a single unrolled join. Merging is not permitted, because that would
117 : * change the meaning, e.g. SEQ_SCAN(a b c d) means simply that sequential
118 : * scans should be used for all of those relations, and is thus equivalent
119 : * to SEQ_SCAN(a b) SEQ_SCAN(c d), but JOIN_ORDER(a b c d) means that "a"
120 : * is the driving table which is then joined to "b" then "c" then "d",
121 : * which is totally different from JOIN_ORDER(a b) and JOIN_ORDER(c d).
122 : */
123 203 : foreach(lc, walker->toplevel_unrolled_joins)
124 : {
125 83 : pgpa_unrolled_join *ujoin = lfirst(lc);
126 :
127 83 : if (buf->len > 0)
128 11 : appendStringInfoChar(buf, '\n');
129 83 : appendStringInfo(context.buf, "JOIN_ORDER(");
130 83 : pgpa_output_unrolled_join(&context, ujoin);
131 83 : appendStringInfoChar(context.buf, ')');
132 83 : pgpa_maybe_linebreak(context.buf, context.wrap_column);
133 : }
134 :
135 : /* Emit join strategy advice. */
136 840 : for (int s = 0; s < NUM_PGPA_JOIN_STRATEGY; ++s)
137 : {
138 720 : char *strategy = pgpa_cstring_join_strategy(s);
139 :
140 720 : pgpa_output_simple_strategy(&context,
141 : strategy,
142 : walker->join_strategies[s]);
143 : }
144 :
145 : /*
146 : * Emit scan strategy advice (but not for ordinary scans, which are
147 : * definitionally uninteresting).
148 : */
149 1080 : for (int c = 0; c < NUM_PGPA_SCAN_STRATEGY; ++c)
150 960 : if (c != PGPA_SCAN_ORDINARY)
151 840 : pgpa_output_scan_strategy(&context, c, walker->scans[c]);
152 :
153 : /* Emit query feature advice. */
154 600 : for (int t = 0; t < NUM_PGPA_QF_TYPES; ++t)
155 480 : pgpa_output_query_feature(&context, t, walker->query_features[t]);
156 :
157 : /* Emit NO_GATHER advice. */
158 120 : pgpa_output_no_gather(&context, walker->no_gather_scans);
159 120 : }
160 :
161 : /*
162 : * Output the members of an unrolled join, first the outermost member, and
163 : * then the inner members one by one, as part of JOIN_ORDER() advice.
164 : */
165 : static void
166 87 : pgpa_output_unrolled_join(pgpa_output_context *context,
167 : pgpa_unrolled_join *join)
168 : {
169 87 : pgpa_output_join_member(context, &join->outer);
170 :
171 203 : for (int k = 0; k < join->ninner; ++k)
172 : {
173 116 : pgpa_join_member *member = &join->inner[k];
174 :
175 116 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
176 116 : appendStringInfoChar(context->buf, ' ');
177 116 : pgpa_output_join_member(context, member);
178 : }
179 87 : }
180 :
181 : /*
182 : * Output a single member of an unrolled join as part of JOIN_ORDER() advice.
183 : */
184 : static void
185 203 : pgpa_output_join_member(pgpa_output_context *context,
186 : pgpa_join_member *member)
187 : {
188 203 : if (member->unrolled_join != NULL)
189 : {
190 4 : appendStringInfoChar(context->buf, '(');
191 4 : pgpa_output_unrolled_join(context, member->unrolled_join);
192 4 : appendStringInfoChar(context->buf, ')');
193 : }
194 : else
195 : {
196 199 : pgpa_scan *scan = member->scan;
197 :
198 : Assert(scan != NULL);
199 199 : if (bms_membership(scan->relids) == BMS_SINGLETON)
200 198 : pgpa_output_relations(context, context->buf, scan->relids);
201 : else
202 : {
203 1 : appendStringInfoChar(context->buf, '{');
204 1 : pgpa_output_relations(context, context->buf, scan->relids);
205 1 : appendStringInfoChar(context->buf, '}');
206 : }
207 : }
208 203 : }
209 :
210 : /*
211 : * Output advice for a List of pgpa_scan objects.
212 : *
213 : * All the scans must use the strategy specified by the "strategy" argument.
214 : */
215 : static void
216 840 : pgpa_output_scan_strategy(pgpa_output_context *context,
217 : pgpa_scan_strategy strategy,
218 : List *scans)
219 : {
220 840 : bool first = true;
221 :
222 840 : if (scans == NIL)
223 694 : return;
224 :
225 146 : if (context->buf->len > 0)
226 98 : appendStringInfoChar(context->buf, '\n');
227 146 : appendStringInfo(context->buf, "%s(",
228 : pgpa_cstring_scan_strategy(strategy));
229 :
230 541 : foreach_ptr(pgpa_scan, scan, scans)
231 : {
232 249 : Plan *plan = scan->plan;
233 :
234 249 : if (first)
235 146 : first = false;
236 : else
237 : {
238 103 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
239 103 : appendStringInfoChar(context->buf, ' ');
240 : }
241 :
242 : /* Output the relation identifiers. */
243 249 : if (bms_membership(scan->relids) == BMS_SINGLETON)
244 244 : pgpa_output_relations(context, context->buf, scan->relids);
245 : else
246 : {
247 5 : appendStringInfoChar(context->buf, '(');
248 5 : pgpa_output_relations(context, context->buf, scan->relids);
249 5 : appendStringInfoChar(context->buf, ')');
250 : }
251 :
252 : /* For index or index-only scans, output index information. */
253 249 : if (strategy == PGPA_SCAN_INDEX)
254 : {
255 : Assert(IsA(plan, IndexScan));
256 65 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
257 65 : appendStringInfoChar(context->buf, ' ');
258 65 : pgpa_output_relation_name(context, ((IndexScan *) plan)->indexid);
259 : }
260 184 : else if (strategy == PGPA_SCAN_INDEX_ONLY)
261 : {
262 : Assert(IsA(plan, IndexOnlyScan));
263 7 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
264 7 : appendStringInfoChar(context->buf, ' ');
265 7 : pgpa_output_relation_name(context,
266 : ((IndexOnlyScan *) plan)->indexid);
267 : }
268 : }
269 :
270 146 : appendStringInfoChar(context->buf, ')');
271 146 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
272 : }
273 :
274 : /*
275 : * Output a schema-qualified relation name.
276 : */
277 : static void
278 72 : pgpa_output_relation_name(pgpa_output_context *context, Oid relid)
279 : {
280 72 : Oid nspoid = get_rel_namespace(relid);
281 72 : char *relnamespace = get_namespace_name_or_temp(nspoid);
282 72 : char *relname = get_rel_name(relid);
283 :
284 72 : appendStringInfoString(context->buf, quote_identifier(relnamespace));
285 72 : appendStringInfoChar(context->buf, '.');
286 72 : appendStringInfoString(context->buf, quote_identifier(relname));
287 72 : }
288 :
289 : /*
290 : * Output advice for a List of pgpa_query_feature objects.
291 : *
292 : * All features must be of the type specified by the "type" argument.
293 : */
294 : static void
295 480 : pgpa_output_query_feature(pgpa_output_context *context, pgpa_qf_type type,
296 : List *query_features)
297 : {
298 480 : bool first = true;
299 :
300 480 : if (query_features == NIL)
301 453 : return;
302 :
303 27 : if (context->buf->len > 0)
304 27 : appendStringInfoChar(context->buf, '\n');
305 27 : appendStringInfo(context->buf, "%s(",
306 : pgpa_cstring_query_feature_type(type));
307 :
308 83 : foreach_ptr(pgpa_query_feature, qf, query_features)
309 : {
310 29 : if (first)
311 27 : first = false;
312 : else
313 : {
314 2 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
315 2 : appendStringInfoChar(context->buf, ' ');
316 : }
317 :
318 29 : if (bms_membership(qf->relids) == BMS_SINGLETON)
319 23 : pgpa_output_relations(context, context->buf, qf->relids);
320 : else
321 : {
322 6 : appendStringInfoChar(context->buf, '(');
323 6 : pgpa_output_relations(context, context->buf, qf->relids);
324 6 : appendStringInfoChar(context->buf, ')');
325 : }
326 : }
327 :
328 27 : appendStringInfoChar(context->buf, ')');
329 27 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
330 : }
331 :
332 : /*
333 : * Output "simple" advice for a List of Bitmapset objects each of which
334 : * contains one or more RTIs.
335 : *
336 : * By simple, we just mean that the advice emitted follows the most
337 : * straightforward pattern: the strategy name, followed by a list of items
338 : * separated by spaces and surrounded by parentheses. Individual items in
339 : * the list are a single relation identifier for a Bitmapset that contains
340 : * just one member, or a sub-list again separated by spaces and surrounded
341 : * by parentheses for a Bitmapset with multiple members. Bitmapsets with
342 : * no members probably shouldn't occur here, but if they do they'll be
343 : * rendered as an empty sub-list.
344 : */
345 : static void
346 720 : pgpa_output_simple_strategy(pgpa_output_context *context, char *strategy,
347 : List *relid_sets)
348 : {
349 720 : bool first = true;
350 :
351 720 : if (relid_sets == NIL)
352 631 : return;
353 :
354 89 : if (context->buf->len > 0)
355 89 : appendStringInfoChar(context->buf, '\n');
356 89 : appendStringInfo(context->buf, "%s(", strategy);
357 :
358 294 : foreach_node(Bitmapset, relids, relid_sets)
359 : {
360 116 : if (first)
361 89 : first = false;
362 : else
363 : {
364 27 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
365 27 : appendStringInfoChar(context->buf, ' ');
366 : }
367 :
368 116 : if (bms_membership(relids) == BMS_SINGLETON)
369 112 : pgpa_output_relations(context, context->buf, relids);
370 : else
371 : {
372 4 : appendStringInfoChar(context->buf, '(');
373 4 : pgpa_output_relations(context, context->buf, relids);
374 4 : appendStringInfoChar(context->buf, ')');
375 : }
376 : }
377 :
378 89 : appendStringInfoChar(context->buf, ')');
379 89 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
380 : }
381 :
382 : /*
383 : * Output NO_GATHER advice for all relations not appearing beneath any
384 : * Gather or Gather Merge node.
385 : */
386 : static void
387 120 : pgpa_output_no_gather(pgpa_output_context *context, Bitmapset *relids)
388 : {
389 120 : if (relids == NULL)
390 8 : return;
391 112 : if (context->buf->len > 0)
392 112 : appendStringInfoChar(context->buf, '\n');
393 112 : appendStringInfoString(context->buf, "NO_GATHER(");
394 112 : pgpa_output_relations(context, context->buf, relids);
395 112 : appendStringInfoChar(context->buf, ')');
396 : }
397 :
398 : /*
399 : * Output the identifiers for each RTI in the provided set.
400 : *
401 : * Identifiers are separated by spaces, and a line break is possible after
402 : * each one.
403 : */
404 : static void
405 705 : pgpa_output_relations(pgpa_output_context *context, StringInfo buf,
406 : Bitmapset *relids)
407 : {
408 705 : int rti = -1;
409 705 : bool first = true;
410 :
411 1562 : while ((rti = bms_next_member(relids, rti)) >= 0)
412 : {
413 857 : const char *rid_string = context->rid_strings[rti - 1];
414 :
415 857 : if (rid_string == NULL)
416 0 : elog(ERROR, "no identifier for RTI %d", rti);
417 :
418 857 : if (first)
419 : {
420 705 : first = false;
421 705 : appendStringInfoString(buf, rid_string);
422 : }
423 : else
424 : {
425 152 : pgpa_maybe_linebreak(buf, context->wrap_column);
426 152 : appendStringInfo(buf, " %s", rid_string);
427 : }
428 : }
429 705 : }
430 :
431 : /*
432 : * Get a C string that corresponds to the specified join strategy.
433 : */
434 : static char *
435 720 : pgpa_cstring_join_strategy(pgpa_join_strategy strategy)
436 : {
437 720 : switch (strategy)
438 : {
439 120 : case JSTRAT_MERGE_JOIN_PLAIN:
440 120 : return "MERGE_JOIN_PLAIN";
441 120 : case JSTRAT_MERGE_JOIN_MATERIALIZE:
442 120 : return "MERGE_JOIN_MATERIALIZE";
443 120 : case JSTRAT_NESTED_LOOP_PLAIN:
444 120 : return "NESTED_LOOP_PLAIN";
445 120 : case JSTRAT_NESTED_LOOP_MATERIALIZE:
446 120 : return "NESTED_LOOP_MATERIALIZE";
447 120 : case JSTRAT_NESTED_LOOP_MEMOIZE:
448 120 : return "NESTED_LOOP_MEMOIZE";
449 120 : case JSTRAT_HASH_JOIN:
450 120 : return "HASH_JOIN";
451 : }
452 :
453 0 : pg_unreachable();
454 : return NULL;
455 : }
456 :
457 : /*
458 : * Get a C string that corresponds to the specified scan strategy.
459 : */
460 : static char *
461 146 : pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy)
462 : {
463 146 : switch (strategy)
464 : {
465 0 : case PGPA_SCAN_ORDINARY:
466 0 : return "ORDINARY_SCAN";
467 78 : case PGPA_SCAN_SEQ:
468 78 : return "SEQ_SCAN";
469 3 : case PGPA_SCAN_BITMAP_HEAP:
470 3 : return "BITMAP_HEAP_SCAN";
471 0 : case PGPA_SCAN_FOREIGN:
472 0 : return "FOREIGN_JOIN";
473 47 : case PGPA_SCAN_INDEX:
474 47 : return "INDEX_SCAN";
475 7 : case PGPA_SCAN_INDEX_ONLY:
476 7 : return "INDEX_ONLY_SCAN";
477 7 : case PGPA_SCAN_PARTITIONWISE:
478 7 : return "PARTITIONWISE";
479 4 : case PGPA_SCAN_TID:
480 4 : return "TID_SCAN";
481 : }
482 :
483 0 : pg_unreachable();
484 : return NULL;
485 : }
486 :
487 : /*
488 : * Get a C string that corresponds to the query feature type.
489 : */
490 : static char *
491 27 : pgpa_cstring_query_feature_type(pgpa_qf_type type)
492 : {
493 27 : switch (type)
494 : {
495 6 : case PGPAQF_GATHER:
496 6 : return "GATHER";
497 7 : case PGPAQF_GATHER_MERGE:
498 7 : return "GATHER_MERGE";
499 5 : case PGPAQF_SEMIJOIN_NON_UNIQUE:
500 5 : return "SEMIJOIN_NON_UNIQUE";
501 9 : case PGPAQF_SEMIJOIN_UNIQUE:
502 9 : return "SEMIJOIN_UNIQUE";
503 : }
504 :
505 :
506 0 : pg_unreachable();
507 : return NULL;
508 : }
509 :
510 : /*
511 : * Insert a line break into the StringInfoData, if needed.
512 : *
513 : * If wrap_column is zero or negative, this does nothing. Otherwise, we
514 : * consider inserting a newline. We only insert a newline if the length of
515 : * the last line in the buffer exceeds wrap_column, and not if we'd be
516 : * inserting a newline at or before the beginning of the current line.
517 : *
518 : * The position at which the newline is inserted is simply wherever the
519 : * buffer ended the last time this function was called. In other words,
520 : * the caller is expected to call this function every time we reach a good
521 : * place for a line break.
522 : */
523 : static void
524 817 : pgpa_maybe_linebreak(StringInfo buf, int wrap_column)
525 : {
526 : char *trailing_nl;
527 : int line_start;
528 : int save_cursor;
529 :
530 : /* If line wrapping is disabled, exit quickly. */
531 817 : if (wrap_column <= 0)
532 0 : return;
533 :
534 : /*
535 : * Set line_start to the byte offset within buf->data of the first
536 : * character of the current line, where the current line means the last
537 : * one in the buffer. Note that line_start could be the offset of the
538 : * trailing '\0' if the last character in the buffer is a line break.
539 : */
540 817 : trailing_nl = strrchr(buf->data, '\n');
541 817 : if (trailing_nl == NULL)
542 243 : line_start = 0;
543 : else
544 574 : line_start = (trailing_nl - buf->data) + 1;
545 :
546 : /*
547 : * Remember that the current end of the buffer is a potential location to
548 : * insert a line break on a future call to this function.
549 : */
550 817 : save_cursor = buf->cursor;
551 817 : buf->cursor = buf->len;
552 :
553 : /* If we haven't passed the wrap column, we don't need a newline. */
554 817 : if (buf->len - line_start <= wrap_column)
555 797 : return;
556 :
557 : /*
558 : * It only makes sense to insert a newline at a position later than the
559 : * beginning of the current line.
560 : */
561 20 : if (save_cursor <= line_start)
562 0 : return;
563 :
564 : /* Insert a newline at the previous cursor location. */
565 20 : enlargeStringInfo(buf, 1);
566 20 : memmove(&buf->data[save_cursor] + 1, &buf->data[save_cursor],
567 20 : buf->len - save_cursor);
568 20 : ++buf->cursor;
569 20 : buf->data[++buf->len] = '\0';
570 20 : buf->data[save_cursor] = '\n';
571 : }
|