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 43664 : pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker,
81 : pgpa_identifier *rt_identifiers)
82 : {
83 43664 : Index rtable_length = list_length(walker->pstmt->rtable);
84 : ListCell *lc;
85 : pgpa_output_context context;
86 :
87 : /* Basic initialization. */
88 43664 : memset(&context, 0, sizeof(pgpa_output_context));
89 43664 : 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 43664 : context.rid_strings = palloc0_array(const char *, rtable_length);
98 142632 : for (int i = 0; i < rtable_length; ++i)
99 98968 : if (rt_identifiers[i].alias_name != NULL)
100 89619 : 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 43664 : context.wrap_column = 76;
115 :
116 : /*
117 : * Each piece of JOIN_ORDER() advice fully describes the join order for a
118 : * 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 54879 : foreach(lc, walker->toplevel_unrolled_joins)
126 : {
127 11215 : pgpa_unrolled_join *ujoin = lfirst(lc);
128 :
129 11215 : if (buf->len > 0)
130 2202 : appendStringInfoChar(buf, '\n');
131 11215 : appendStringInfoString(context.buf, "JOIN_ORDER(");
132 11215 : pgpa_output_unrolled_join(&context, ujoin);
133 11215 : appendStringInfoChar(context.buf, ')');
134 11215 : pgpa_maybe_linebreak(context.buf, context.wrap_column);
135 : }
136 :
137 : /* Emit join strategy advice. */
138 305648 : for (int s = 0; s < NUM_PGPA_JOIN_STRATEGY; ++s)
139 : {
140 261984 : char *strategy = pgpa_cstring_join_strategy(s);
141 :
142 261984 : 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 392976 : for (int c = 0; c < NUM_PGPA_SCAN_STRATEGY; ++c)
152 349312 : if (c != PGPA_SCAN_ORDINARY)
153 305648 : pgpa_output_scan_strategy(&context, c, walker->scans[c]);
154 :
155 : /* Emit query feature advice. */
156 218320 : for (int t = 0; t < NUM_PGPA_QF_TYPES; ++t)
157 174656 : pgpa_output_query_feature(&context, t, walker->query_features[t]);
158 :
159 : /* Emit NO_GATHER advice. */
160 43664 : pgpa_output_no_gather(&context, walker->no_gather_scans);
161 :
162 : /* Emit DO_NOT_SCAN advice. */
163 43664 : pgpa_output_do_not_scan(&context, walker->do_not_scan_identifiers);
164 43664 : }
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 11749 : pgpa_output_unrolled_join(pgpa_output_context *context,
172 : pgpa_unrolled_join *join)
173 : {
174 11749 : pgpa_output_join_member(context, &join->outer);
175 :
176 26662 : for (int k = 0; k < join->ninner; ++k)
177 : {
178 14913 : pgpa_join_member *member = &join->inner[k];
179 :
180 14913 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
181 14913 : appendStringInfoChar(context->buf, ' ');
182 14913 : pgpa_output_join_member(context, member);
183 : }
184 11749 : }
185 :
186 : /*
187 : * Output a single member of an unrolled join as part of JOIN_ORDER() advice.
188 : */
189 : static void
190 26662 : pgpa_output_join_member(pgpa_output_context *context,
191 : pgpa_join_member *member)
192 : {
193 26662 : if (member->unrolled_join != NULL)
194 : {
195 534 : appendStringInfoChar(context->buf, '(');
196 534 : pgpa_output_unrolled_join(context, member->unrolled_join);
197 534 : appendStringInfoChar(context->buf, ')');
198 : }
199 : else
200 : {
201 26128 : pgpa_scan *scan = member->scan;
202 :
203 : Assert(scan != NULL);
204 26128 : if (bms_membership(scan->relids) == BMS_SINGLETON)
205 26114 : 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 26662 : }
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 305648 : pgpa_output_scan_strategy(pgpa_output_context *context,
222 : pgpa_scan_strategy strategy,
223 : List *scans)
224 : {
225 305648 : bool first = true;
226 :
227 305648 : if (scans == NIL)
228 275749 : return;
229 :
230 29899 : if (context->buf->len > 0)
231 15597 : appendStringInfoChar(context->buf, '\n');
232 29899 : appendStringInfo(context->buf, "%s(",
233 : pgpa_cstring_scan_strategy(strategy));
234 :
235 105903 : foreach_ptr(pgpa_scan, scan, scans)
236 : {
237 46105 : Plan *plan = scan->plan;
238 :
239 46105 : if (first)
240 29899 : first = false;
241 : else
242 : {
243 16206 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
244 16206 : appendStringInfoChar(context->buf, ' ');
245 : }
246 :
247 : /* Output the relation identifiers. */
248 46105 : if (bms_membership(scan->relids) == BMS_SINGLETON)
249 45900 : 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 46105 : if (strategy == PGPA_SCAN_INDEX)
259 : {
260 : Assert(IsA(plan, IndexScan));
261 11846 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
262 11846 : appendStringInfoChar(context->buf, ' ');
263 11846 : pgpa_output_relation_name(context, ((IndexScan *) plan)->indexid);
264 : }
265 34259 : else if (strategy == PGPA_SCAN_INDEX_ONLY)
266 : {
267 : Assert(IsA(plan, IndexOnlyScan));
268 1928 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
269 1928 : appendStringInfoChar(context->buf, ' ');
270 1928 : pgpa_output_relation_name(context,
271 : ((IndexOnlyScan *) plan)->indexid);
272 : }
273 : }
274 :
275 29899 : appendStringInfoChar(context->buf, ')');
276 29899 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
277 : }
278 :
279 : /*
280 : * Output a schema-qualified relation name.
281 : */
282 : static void
283 13774 : pgpa_output_relation_name(pgpa_output_context *context, Oid relid)
284 : {
285 13774 : Oid nspoid = get_rel_namespace(relid);
286 13774 : char *relnamespace = get_namespace_name_or_temp(nspoid);
287 13774 : char *relname = get_rel_name(relid);
288 :
289 13774 : appendStringInfoString(context->buf, quote_identifier(relnamespace));
290 13774 : appendStringInfoChar(context->buf, '.');
291 13774 : appendStringInfoString(context->buf, quote_identifier(relname));
292 13774 : }
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 174656 : pgpa_output_query_feature(pgpa_output_context *context, pgpa_qf_type type,
301 : List *query_features)
302 : {
303 174656 : bool first = true;
304 :
305 174656 : if (query_features == NIL)
306 173786 : return;
307 :
308 870 : if (context->buf->len > 0)
309 869 : appendStringInfoChar(context->buf, '\n');
310 870 : appendStringInfo(context->buf, "%s(",
311 : pgpa_cstring_query_feature_type(type));
312 :
313 2680 : foreach_ptr(pgpa_query_feature, qf, query_features)
314 : {
315 940 : if (first)
316 870 : first = false;
317 : else
318 : {
319 70 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
320 70 : appendStringInfoChar(context->buf, ' ');
321 : }
322 :
323 940 : if (bms_membership(qf->relids) == BMS_SINGLETON)
324 836 : pgpa_output_relations(context, context->buf, qf->relids);
325 : else
326 : {
327 104 : appendStringInfoChar(context->buf, '(');
328 104 : pgpa_output_relations(context, context->buf, qf->relids);
329 104 : appendStringInfoChar(context->buf, ')');
330 : }
331 : }
332 :
333 870 : appendStringInfoChar(context->buf, ')');
334 870 : 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 261984 : pgpa_output_simple_strategy(pgpa_output_context *context, char *strategy,
352 : List *relid_sets)
353 : {
354 261984 : bool first = true;
355 :
356 261984 : if (relid_sets == NIL)
357 251899 : return;
358 :
359 10085 : if (context->buf->len > 0)
360 10085 : appendStringInfoChar(context->buf, '\n');
361 10085 : appendStringInfo(context->buf, "%s(", strategy);
362 :
363 35083 : foreach_node(Bitmapset, relids, relid_sets)
364 : {
365 14913 : if (first)
366 10085 : first = false;
367 : else
368 : {
369 4828 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
370 4828 : appendStringInfoChar(context->buf, ' ');
371 : }
372 :
373 14913 : if (bms_membership(relids) == BMS_SINGLETON)
374 14368 : pgpa_output_relations(context, context->buf, relids);
375 : else
376 : {
377 545 : appendStringInfoChar(context->buf, '(');
378 545 : pgpa_output_relations(context, context->buf, relids);
379 545 : appendStringInfoChar(context->buf, ')');
380 : }
381 : }
382 :
383 10085 : appendStringInfoChar(context->buf, ')');
384 10085 : 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 43664 : pgpa_output_no_gather(pgpa_output_context *context, Bitmapset *relids)
393 : {
394 43664 : if (relids == NULL)
395 178 : return;
396 43486 : if (context->buf->len > 0)
397 23143 : appendStringInfoChar(context->buf, '\n');
398 43486 : appendStringInfoString(context->buf, "NO_GATHER(");
399 43486 : pgpa_output_relations(context, context->buf, relids);
400 43486 : 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 43664 : pgpa_output_do_not_scan(pgpa_output_context *context, List *identifiers)
409 : {
410 43664 : bool first = true;
411 :
412 43664 : if (identifiers == NIL)
413 43440 : return;
414 224 : if (context->buf->len > 0)
415 224 : appendStringInfoChar(context->buf, '\n');
416 224 : appendStringInfoString(context->buf, "DO_NOT_SCAN(");
417 :
418 880 : foreach_ptr(pgpa_identifier, rid, identifiers)
419 : {
420 432 : if (first)
421 224 : first = false;
422 : else
423 : {
424 208 : pgpa_maybe_linebreak(context->buf, context->wrap_column);
425 208 : appendStringInfoChar(context->buf, ' ');
426 : }
427 432 : appendStringInfoString(context->buf, pgpa_identifier_string(rid));
428 : }
429 :
430 224 : 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 131572 : pgpa_output_relations(pgpa_output_context *context, StringInfo buf,
441 : Bitmapset *relids)
442 : {
443 131572 : int rti = -1;
444 131572 : bool first = true;
445 :
446 292175 : while ((rti = bms_next_member(relids, rti)) >= 0)
447 : {
448 160603 : const char *rid_string = context->rid_strings[rti - 1];
449 :
450 160603 : if (rid_string == NULL)
451 0 : elog(ERROR, "no identifier for RTI %d", rti);
452 :
453 160603 : if (first)
454 : {
455 131572 : first = false;
456 131572 : appendStringInfoString(buf, rid_string);
457 : }
458 : else
459 : {
460 29031 : pgpa_maybe_linebreak(buf, context->wrap_column);
461 29031 : appendStringInfo(buf, " %s", rid_string);
462 : }
463 : }
464 131572 : }
465 :
466 : /*
467 : * Get a C string that corresponds to the specified join strategy.
468 : */
469 : static char *
470 261984 : pgpa_cstring_join_strategy(pgpa_join_strategy strategy)
471 : {
472 261984 : switch (strategy)
473 : {
474 43664 : case JSTRAT_MERGE_JOIN_PLAIN:
475 43664 : return "MERGE_JOIN_PLAIN";
476 43664 : case JSTRAT_MERGE_JOIN_MATERIALIZE:
477 43664 : return "MERGE_JOIN_MATERIALIZE";
478 43664 : case JSTRAT_NESTED_LOOP_PLAIN:
479 43664 : return "NESTED_LOOP_PLAIN";
480 43664 : case JSTRAT_NESTED_LOOP_MATERIALIZE:
481 43664 : return "NESTED_LOOP_MATERIALIZE";
482 43664 : case JSTRAT_NESTED_LOOP_MEMOIZE:
483 43664 : return "NESTED_LOOP_MEMOIZE";
484 43664 : case JSTRAT_HASH_JOIN:
485 43664 : 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 29899 : pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy)
497 : {
498 29899 : switch (strategy)
499 : {
500 0 : case PGPA_SCAN_ORDINARY:
501 0 : return "ORDINARY_SCAN";
502 16544 : case PGPA_SCAN_SEQ:
503 16544 : return "SEQ_SCAN";
504 2553 : case PGPA_SCAN_BITMAP_HEAP:
505 2553 : return "BITMAP_HEAP_SCAN";
506 0 : case PGPA_SCAN_FOREIGN:
507 0 : return "FOREIGN_JOIN";
508 7018 : case PGPA_SCAN_INDEX:
509 7018 : return "INDEX_SCAN";
510 1709 : case PGPA_SCAN_INDEX_ONLY:
511 1709 : return "INDEX_ONLY_SCAN";
512 1673 : case PGPA_SCAN_PARTITIONWISE:
513 1673 : return "PARTITIONWISE";
514 402 : case PGPA_SCAN_TID:
515 402 : 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 870 : pgpa_cstring_query_feature_type(pgpa_qf_type type)
527 : {
528 870 : switch (type)
529 : {
530 161 : case PGPAQF_GATHER:
531 161 : 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 71 : case PGPAQF_SEMIJOIN_UNIQUE:
537 71 : 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 131099 : 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 131099 : 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 131099 : trailing_nl = strrchr(buf->data, '\n');
576 131099 : if (trailing_nl == NULL)
577 41034 : line_start = 0;
578 : else
579 90065 : 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 131099 : save_cursor = buf->cursor;
586 131099 : buf->cursor = buf->len;
587 :
588 : /* If we haven't passed the wrap column, we don't need a newline. */
589 131099 : if (buf->len - line_start <= wrap_column)
590 122628 : 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 8471 : if (save_cursor <= line_start)
597 0 : return;
598 :
599 : /* Insert a newline at the previous cursor location. */
600 8471 : enlargeStringInfo(buf, 1);
601 8471 : memmove(&buf->data[save_cursor] + 1, &buf->data[save_cursor],
602 8471 : buf->len - save_cursor);
603 8471 : ++buf->cursor;
604 8471 : buf->data[++buf->len] = '\0';
605 8471 : buf->data[save_cursor] = '\n';
606 : }
|