PostgreSQL Source Code git master
Loading...
Searching...
No Matches
pgpa_output.c
Go to the documentation of this file.
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 */
40
42 pgpa_unrolled_join *join);
44 pgpa_join_member *member);
46 pgpa_scan_strategy strategy,
47 List *scans);
48static void pgpa_output_relation_name(pgpa_output_context *context, Oid relid);
51 List *query_features);
53 char *strategy,
56 Bitmapset *relids);
58 List *identifiers);
60 Bitmapset *relids);
61
65
66static 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 */
79void
82{
83 Index rtable_length = list_length(walker->pstmt->rtable);
84 ListCell *lc;
85 pgpa_output_context context;
86
87 /* Basic initialization. */
88 memset(&context, 0, sizeof(pgpa_output_context));
89 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 context.rid_strings = palloc0_array(const char *, rtable_length);
98 for (int i = 0; i < rtable_length; ++i)
99 if (rt_identifiers[i].alias_name != NULL)
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 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 foreach(lc, walker->toplevel_unrolled_joins)
126 {
128
129 if (buf->len > 0)
131 appendStringInfo(context.buf, "JOIN_ORDER(");
133 appendStringInfoChar(context.buf, ')');
134 pgpa_maybe_linebreak(context.buf, context.wrap_column);
135 }
136
137 /* Emit join strategy advice. */
138 for (int s = 0; s < NUM_PGPA_JOIN_STRATEGY; ++s)
139 {
140 char *strategy = pgpa_cstring_join_strategy(s);
141
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 for (int c = 0; c < NUM_PGPA_SCAN_STRATEGY; ++c)
152 if (c != PGPA_SCAN_ORDINARY)
153 pgpa_output_scan_strategy(&context, c, walker->scans[c]);
154
155 /* Emit query feature advice. */
156 for (int t = 0; t < NUM_PGPA_QF_TYPES; ++t)
157 pgpa_output_query_feature(&context, t, walker->query_features[t]);
158
159 /* Emit NO_GATHER advice. */
160 pgpa_output_no_gather(&context, walker->no_gather_scans);
161
162 /* Emit DO_NOT_SCAN advice. */
163 pgpa_output_do_not_scan(&context, walker->do_not_scan_identifiers);
164}
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 */
170static void
172 pgpa_unrolled_join *join)
173{
174 pgpa_output_join_member(context, &join->outer);
175
176 for (int k = 0; k < join->ninner; ++k)
177 {
178 pgpa_join_member *member = &join->inner[k];
179
180 pgpa_maybe_linebreak(context->buf, context->wrap_column);
181 appendStringInfoChar(context->buf, ' ');
182 pgpa_output_join_member(context, member);
183 }
184}
185
186/*
187 * Output a single member of an unrolled join as part of JOIN_ORDER() advice.
188 */
189static void
191 pgpa_join_member *member)
192{
193 if (member->unrolled_join != NULL)
194 {
195 appendStringInfoChar(context->buf, '(');
197 appendStringInfoChar(context->buf, ')');
198 }
199 else
200 {
201 pgpa_scan *scan = member->scan;
202
203 Assert(scan != NULL);
204 if (bms_membership(scan->relids) == BMS_SINGLETON)
205 pgpa_output_relations(context, context->buf, scan->relids);
206 else
207 {
208 appendStringInfoChar(context->buf, '{');
209 pgpa_output_relations(context, context->buf, scan->relids);
210 appendStringInfoChar(context->buf, '}');
211 }
212 }
213}
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 */
220static void
222 pgpa_scan_strategy strategy,
223 List *scans)
224{
225 bool first = true;
226
227 if (scans == NIL)
228 return;
229
230 if (context->buf->len > 0)
231 appendStringInfoChar(context->buf, '\n');
232 appendStringInfo(context->buf, "%s(",
234
235 foreach_ptr(pgpa_scan, scan, scans)
236 {
237 Plan *plan = scan->plan;
238
239 if (first)
240 first = false;
241 else
242 {
243 pgpa_maybe_linebreak(context->buf, context->wrap_column);
244 appendStringInfoChar(context->buf, ' ');
245 }
246
247 /* Output the relation identifiers. */
248 if (bms_membership(scan->relids) == BMS_SINGLETON)
249 pgpa_output_relations(context, context->buf, scan->relids);
250 else
251 {
252 appendStringInfoChar(context->buf, '(');
253 pgpa_output_relations(context, context->buf, scan->relids);
254 appendStringInfoChar(context->buf, ')');
255 }
256
257 /* For index or index-only scans, output index information. */
258 if (strategy == PGPA_SCAN_INDEX)
259 {
261 pgpa_maybe_linebreak(context->buf, context->wrap_column);
262 appendStringInfoChar(context->buf, ' ');
263 pgpa_output_relation_name(context, ((IndexScan *) plan)->indexid);
264 }
265 else if (strategy == PGPA_SCAN_INDEX_ONLY)
266 {
268 pgpa_maybe_linebreak(context->buf, context->wrap_column);
269 appendStringInfoChar(context->buf, ' ');
271 ((IndexOnlyScan *) plan)->indexid);
272 }
273 }
274
275 appendStringInfoChar(context->buf, ')');
276 pgpa_maybe_linebreak(context->buf, context->wrap_column);
277}
278
279/*
280 * Output a schema-qualified relation name.
281 */
282static void
284{
286 char *relnamespace = get_namespace_name_or_temp(nspoid);
287 char *relname = get_rel_name(relid);
288
289 appendStringInfoString(context->buf, quote_identifier(relnamespace));
290 appendStringInfoChar(context->buf, '.');
292}
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 */
299static void
301 List *query_features)
302{
303 bool first = true;
304
305 if (query_features == NIL)
306 return;
307
308 if (context->buf->len > 0)
309 appendStringInfoChar(context->buf, '\n');
310 appendStringInfo(context->buf, "%s(",
312
313 foreach_ptr(pgpa_query_feature, qf, query_features)
314 {
315 if (first)
316 first = false;
317 else
318 {
319 pgpa_maybe_linebreak(context->buf, context->wrap_column);
320 appendStringInfoChar(context->buf, ' ');
321 }
322
323 if (bms_membership(qf->relids) == BMS_SINGLETON)
324 pgpa_output_relations(context, context->buf, qf->relids);
325 else
326 {
327 appendStringInfoChar(context->buf, '(');
328 pgpa_output_relations(context, context->buf, qf->relids);
329 appendStringInfoChar(context->buf, ')');
330 }
331 }
332
333 appendStringInfoChar(context->buf, ')');
334 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 */
350static void
353{
354 bool first = true;
355
356 if (relid_sets == NIL)
357 return;
358
359 if (context->buf->len > 0)
360 appendStringInfoChar(context->buf, '\n');
361 appendStringInfo(context->buf, "%s(", strategy);
362
364 {
365 if (first)
366 first = false;
367 else
368 {
369 pgpa_maybe_linebreak(context->buf, context->wrap_column);
370 appendStringInfoChar(context->buf, ' ');
371 }
372
373 if (bms_membership(relids) == BMS_SINGLETON)
374 pgpa_output_relations(context, context->buf, relids);
375 else
376 {
377 appendStringInfoChar(context->buf, '(');
378 pgpa_output_relations(context, context->buf, relids);
379 appendStringInfoChar(context->buf, ')');
380 }
381 }
382
383 appendStringInfoChar(context->buf, ')');
384 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 */
391static void
393{
394 if (relids == NULL)
395 return;
396 if (context->buf->len > 0)
397 appendStringInfoChar(context->buf, '\n');
398 appendStringInfoString(context->buf, "NO_GATHER(");
399 pgpa_output_relations(context, context->buf, relids);
400 appendStringInfoChar(context->buf, ')');
401}
402
403/*
404 * Output DO_NOT_SCAN advice for all relations in the provided list of
405 * identifiers.
406 */
407static void
409{
410 bool first = true;
411
412 if (identifiers == NIL)
413 return;
414 if (context->buf->len > 0)
415 appendStringInfoChar(context->buf, '\n');
416 appendStringInfoString(context->buf, "DO_NOT_SCAN(");
417
418 foreach_ptr(pgpa_identifier, rid, identifiers)
419 {
420 if (first)
421 first = false;
422 else
423 {
424 pgpa_maybe_linebreak(context->buf, context->wrap_column);
425 appendStringInfoChar(context->buf, ' ');
426 }
428 }
429
430 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 */
439static void
441 Bitmapset *relids)
442{
443 int rti = -1;
444 bool first = true;
445
446 while ((rti = bms_next_member(relids, rti)) >= 0)
447 {
448 const char *rid_string = context->rid_strings[rti - 1];
449
450 if (rid_string == NULL)
451 elog(ERROR, "no identifier for RTI %d", rti);
452
453 if (first)
454 {
455 first = false;
457 }
458 else
459 {
462 }
463 }
464}
465
466/*
467 * Get a C string that corresponds to the specified join strategy.
468 */
469static char *
471{
472 switch (strategy)
473 {
475 return "MERGE_JOIN_PLAIN";
477 return "MERGE_JOIN_MATERIALIZE";
479 return "NESTED_LOOP_PLAIN";
481 return "NESTED_LOOP_MATERIALIZE";
483 return "NESTED_LOOP_MEMOIZE";
484 case JSTRAT_HASH_JOIN:
485 return "HASH_JOIN";
486 }
487
489 return NULL;
490}
491
492/*
493 * Get a C string that corresponds to the specified scan strategy.
494 */
495static char *
497{
498 switch (strategy)
499 {
501 return "ORDINARY_SCAN";
502 case PGPA_SCAN_SEQ:
503 return "SEQ_SCAN";
505 return "BITMAP_HEAP_SCAN";
507 return "FOREIGN_JOIN";
508 case PGPA_SCAN_INDEX:
509 return "INDEX_SCAN";
511 return "INDEX_ONLY_SCAN";
513 return "PARTITIONWISE";
514 case PGPA_SCAN_TID:
515 return "TID_SCAN";
516 }
517
519 return NULL;
520}
521
522/*
523 * Get a C string that corresponds to the query feature type.
524 */
525static char *
527{
528 switch (type)
529 {
530 case PGPAQF_GATHER:
531 return "GATHER";
533 return "GATHER_MERGE";
535 return "SEMIJOIN_NON_UNIQUE";
537 return "SEMIJOIN_UNIQUE";
538 }
539
540
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 */
558static void
560{
561 char *trailing_nl;
562 int line_start;
563 int save_cursor;
564
565 /* If line wrapping is disabled, exit quickly. */
566 if (wrap_column <= 0)
567 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 trailing_nl = strrchr(buf->data, '\n');
576 if (trailing_nl == NULL)
577 line_start = 0;
578 else
579 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 save_cursor = buf->cursor;
586 buf->cursor = buf->len;
587
588 /* If we haven't passed the wrap column, we don't need a newline. */
589 if (buf->len - line_start <= wrap_column)
590 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 if (save_cursor <= line_start)
597 return;
598
599 /* Insert a newline at the previous cursor location. */
601 memmove(&buf->data[save_cursor] + 1, &buf->data[save_cursor],
602 buf->len - save_cursor);
603 ++buf->cursor;
604 buf->data[++buf->len] = '\0';
605 buf->data[save_cursor] = '\n';
606}
int bms_next_member(const Bitmapset *a, int prevbit)
Definition bitmapset.c:1290
BMS_Membership bms_membership(const Bitmapset *a)
Definition bitmapset.c:765
@ BMS_SINGLETON
Definition bitmapset.h:72
#define Assert(condition)
Definition c.h:943
#define pg_unreachable()
Definition c.h:367
unsigned int Index
Definition c.h:698
#define ERROR
Definition elog.h:39
#define elog(elevel,...)
Definition elog.h:227
#define palloc0_array(type, count)
Definition fe_memutils.h:77
int i
Definition isn.c:77
char * get_rel_name(Oid relid)
Definition lsyscache.c:2148
Oid get_rel_namespace(Oid relid)
Definition lsyscache.c:2172
char * get_namespace_name_or_temp(Oid nspid)
Definition lsyscache.c:3612
#define IsA(nodeptr, _type_)
Definition nodes.h:164
NameData relname
Definition pg_class.h:40
#define lfirst(lc)
Definition pg_list.h:172
static int list_length(const List *l)
Definition pg_list.h:152
#define NIL
Definition pg_list.h:68
#define foreach_ptr(type, var, lst)
Definition pg_list.h:501
#define foreach_node(type, var, lst)
Definition pg_list.h:528
#define plan(x)
Definition pg_regress.c:164
static char buf[DEFAULT_XLOG_SEG_SIZE]
const char * pgpa_identifier_string(const pgpa_identifier *rid)
pgpa_join_strategy
Definition pgpa_join.h:28
@ JSTRAT_MERGE_JOIN_PLAIN
Definition pgpa_join.h:29
@ JSTRAT_NESTED_LOOP_MATERIALIZE
Definition pgpa_join.h:32
@ JSTRAT_NESTED_LOOP_MEMOIZE
Definition pgpa_join.h:33
@ JSTRAT_HASH_JOIN
Definition pgpa_join.h:34
@ JSTRAT_NESTED_LOOP_PLAIN
Definition pgpa_join.h:31
@ JSTRAT_MERGE_JOIN_MATERIALIZE
Definition pgpa_join.h:30
#define NUM_PGPA_JOIN_STRATEGY
Definition pgpa_join.h:38
void pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker, pgpa_identifier *rt_identifiers)
Definition pgpa_output.c:80
static void pgpa_output_simple_strategy(pgpa_output_context *context, char *strategy, List *relid_sets)
static char * pgpa_cstring_join_strategy(pgpa_join_strategy strategy)
static void pgpa_output_relations(pgpa_output_context *context, StringInfo buf, Bitmapset *relids)
static void pgpa_output_no_gather(pgpa_output_context *context, Bitmapset *relids)
static void pgpa_maybe_linebreak(StringInfo buf, int wrap_column)
static void pgpa_output_unrolled_join(pgpa_output_context *context, pgpa_unrolled_join *join)
static void pgpa_output_relation_name(pgpa_output_context *context, Oid relid)
static void pgpa_output_scan_strategy(pgpa_output_context *context, pgpa_scan_strategy strategy, List *scans)
static void pgpa_output_query_feature(pgpa_output_context *context, pgpa_qf_type type, List *query_features)
static void pgpa_output_join_member(pgpa_output_context *context, pgpa_join_member *member)
static char * pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy)
static char * pgpa_cstring_query_feature_type(pgpa_qf_type type)
static void pgpa_output_do_not_scan(pgpa_output_context *context, List *identifiers)
pgpa_scan_strategy
Definition pgpa_scan.h:56
@ PGPA_SCAN_SEQ
Definition pgpa_scan.h:58
@ PGPA_SCAN_INDEX
Definition pgpa_scan.h:61
@ PGPA_SCAN_INDEX_ONLY
Definition pgpa_scan.h:62
@ PGPA_SCAN_BITMAP_HEAP
Definition pgpa_scan.h:59
@ PGPA_SCAN_FOREIGN
Definition pgpa_scan.h:60
@ PGPA_SCAN_TID
Definition pgpa_scan.h:64
@ PGPA_SCAN_PARTITIONWISE
Definition pgpa_scan.h:63
@ PGPA_SCAN_ORDINARY
Definition pgpa_scan.h:57
#define NUM_PGPA_SCAN_STRATEGY
Definition pgpa_scan.h:68
pgpa_qf_type
Definition pgpa_walker.h:44
@ PGPAQF_GATHER
Definition pgpa_walker.h:45
@ PGPAQF_GATHER_MERGE
Definition pgpa_walker.h:46
@ PGPAQF_SEMIJOIN_UNIQUE
Definition pgpa_walker.h:48
@ PGPAQF_SEMIJOIN_NON_UNIQUE
Definition pgpa_walker.h:47
#define NUM_PGPA_QF_TYPES
Definition pgpa_walker.h:52
unsigned int Oid
char * c
static int fb(int x)
const char * quote_identifier(const char *ident)
void appendStringInfo(StringInfo str, const char *fmt,...)
Definition stringinfo.c:145
void enlargeStringInfo(StringInfo str, int needed)
Definition stringinfo.c:337
void appendStringInfoString(StringInfo str, const char *s)
Definition stringinfo.c:230
void appendStringInfoChar(StringInfo str, char ch)
Definition stringinfo.c:242
Definition pg_list.h:54
struct pgpa_scan * scan
Definition pgpa_join.h:60
pgpa_unrolled_join * unrolled_join
Definition pgpa_join.h:61
const char ** rid_strings
Definition pgpa_output.c:36
Bitmapset * relids
Definition pgpa_scan.h:77
pgpa_join_member * inner
Definition pgpa_join.h:83
pgpa_join_member outer
Definition pgpa_join.h:74
const char * type