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