Hi, and sorry to take such a long break from this patch. On Wed, Apr 14, 2021 at 02:27:36PM +0300, e.sokol...@postgrespro.ru wrote: > diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c > index b62a76e7e5a..bf8c37baefd 100644 > --- a/src/backend/commands/explain.c > +++ b/src/backend/commands/explain.c > @@ -1615,6 +1615,11 @@ ExplainNode(PlanState *planstate, List *ancestors, > double startup_ms = 1000.0 * > planstate->instrument->startup / nloops; > double total_ms = 1000.0 * > planstate->instrument->total / nloops; > double rows = planstate->instrument->ntuples / nloops; > + double loop_total_rows = > planstate->instrument->ntuples; > + double loop_min_r = planstate->instrument->min_tuples; > + double loop_max_r = planstate->instrument->max_tuples; > + double loop_min_t_ms = 1000.0 * > planstate->instrument->min_t; > + double loop_max_t_ms = 1000.0 * > planstate->instrument->max_t; > > if (es->format == EXPLAIN_FORMAT_TEXT) > { > @@ -1626,6 +1631,19 @@ ExplainNode(PlanState *planstate, List *ancestors, > appendStringInfo(es->str, > " (actual > rows=%.0f loops=%.0f)", > rows, nloops); > + if (nloops > 1 && es->verbose) > + { > + appendStringInfo(es->str, "\n"); > + ExplainIndentText(es); > + if (es->timing) > + appendStringInfo(es->str, > + "Loop > min_time: %.3f max_time: %.3f min_rows: %.0f max_rows: %.0f total_rows: > %.0f", > + > loop_min_t_ms, loop_max_t_ms, loop_min_r, loop_max_r, loop_total_rows);
Now that I see it, I think it should say it with spaces, and not underscores, like | Loop Min Time: %.3f Max Time: %.3f ... "Memory Usage:" already has spaces in its fields names, so this is more consistent, and isn't doing anything new. I think the min/max/total should be first, and the timing should follow, if enabled. The "if(timing)" doesn't even need to duplicate the output, it could append just the timing part. I refactored this all into a separate function. I don't see why we'd repeat these. + double loop_total_rows = planstate->instrument->ntuples; + double loop_min_r = planstate->instrument->min_tuples; + double loop_max_r = planstate->instrument->max_tuples; + double loop_min_t_ms = 1000.0 * planstate->instrument->min_t; + double loop_max_t_ms = 1000.0 * planstate->instrument->max_t; I realize the duplication doesn't originate with your patch. But because of the duplication, there can be inconsistencies; for example, you wrote "ms" in one place and "s" in another. Maybe you copied from before f90c708a048667befbf6bbe5f48ae9695cb89de4 (an issue I reported the first time I was looking at this patch). I think the non-text format timing stuff needs to be within "if (timing)". I'm curious to hear what you and others think of the refactoring. It'd be nice if there's a good way to add a test case for verbose output involving parallel workers, but the output is unstable ... -- Justin
>From e9dc405c55507b6c93a0e66434c5757254e95d54 Mon Sep 17 00:00:00 2001 From: Justin Pryzby <pryz...@telsasoft.com> Date: Thu, 15 Apr 2021 11:55:09 -0500 Subject: [PATCH 1/3] explain.c: refactor ExplainNode() --- src/backend/commands/explain.c | 110 ++++++++++++++------------------- 1 file changed, 47 insertions(+), 63 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 891ad0e717..db99739cc5 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -118,6 +118,8 @@ static void show_instrumentation_count(const char *qlabel, int which, PlanState *planstate, ExplainState *es); static void show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es); static void show_eval_params(Bitmapset *bms_params, ExplainState *es); +static void show_loop_info(Instrumentation *instrument, bool isworker, + ExplainState *es); static const char *explain_get_index_name(Oid indexId); static void show_buffer_usage(ExplainState *es, const BufferUsage *usage, bool planning); @@ -1609,36 +1611,7 @@ ExplainNode(PlanState *planstate, List *ancestors, if (es->analyze && planstate->instrument && planstate->instrument->nloops > 0) - { - double nloops = planstate->instrument->nloops; - double startup_ms = 1000.0 * planstate->instrument->startup / nloops; - double total_ms = 1000.0 * planstate->instrument->total / nloops; - double rows = planstate->instrument->ntuples / nloops; - - if (es->format == EXPLAIN_FORMAT_TEXT) - { - if (es->timing) - appendStringInfo(es->str, - " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)", - startup_ms, total_ms, rows, nloops); - else - appendStringInfo(es->str, - " (actual rows=%.0f loops=%.0f)", - rows, nloops); - } - else - { - if (es->timing) - { - ExplainPropertyFloat("Actual Startup Time", "ms", startup_ms, - 3, es); - ExplainPropertyFloat("Actual Total Time", "ms", total_ms, - 3, es); - } - ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es); - ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es); - } - } + show_loop_info(planstate->instrument, false, es); else if (es->analyze) { if (es->format == EXPLAIN_FORMAT_TEXT) @@ -1667,44 +1640,14 @@ ExplainNode(PlanState *planstate, List *ancestors, for (int n = 0; n < w->num_workers; n++) { Instrumentation *instrument = &w->instrument[n]; - double nloops = instrument->nloops; - double startup_ms; - double total_ms; - double rows; - if (nloops <= 0) + if (instrument->nloops <= 0) continue; - startup_ms = 1000.0 * instrument->startup / nloops; - total_ms = 1000.0 * instrument->total / nloops; - rows = instrument->ntuples / nloops; ExplainOpenWorker(n, es); - + show_loop_info(instrument, true, es); if (es->format == EXPLAIN_FORMAT_TEXT) - { - ExplainIndentText(es); - if (es->timing) - appendStringInfo(es->str, - "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n", - startup_ms, total_ms, rows, nloops); - else - appendStringInfo(es->str, - "actual rows=%.0f loops=%.0f\n", - rows, nloops); - } - else - { - if (es->timing) - { - ExplainPropertyFloat("Actual Startup Time", "ms", - startup_ms, 3, es); - ExplainPropertyFloat("Actual Total Time", "ms", - total_ms, 3, es); - } - ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es); - ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es); - } - + appendStringInfoChar(es->str, '\n'); ExplainCloseWorker(n, es); } } @@ -4030,6 +3973,47 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, ExplainCloseGroup("Target Tables", "Target Tables", false, es); } +void +show_loop_info(Instrumentation *instrument, bool isworker, ExplainState *es) +{ + double nloops = instrument->nloops; + double startup_ms = 1000.0 * instrument->startup / nloops; + double total_ms = 1000.0 * instrument->total / nloops; + double rows = instrument->ntuples / nloops; + + if (es->format == EXPLAIN_FORMAT_TEXT) + { + if (isworker) + ExplainIndentText(es); + else + appendStringInfo(es->str, " ("); + + if (es->timing) + appendStringInfo(es->str, + "actual time=%.3f..%.3f rows=%.0f loops=%.0f", + startup_ms, total_ms, rows, nloops); + else + appendStringInfo(es->str, + "actual rows=%.0f loops=%.0f", + rows, nloops); + + if (!isworker) + appendStringInfoChar(es->str, ')'); + } + else + { + if (es->timing) + { + ExplainPropertyFloat("Actual Startup Time", "ms", startup_ms, + 3, es); + ExplainPropertyFloat("Actual Total Time", "ms", total_ms, + 3, es); + } + ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es); + ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es); + } +} + /* * Explain the constituent plans of an Append, MergeAppend, * BitmapAnd, or BitmapOr node. -- 2.17.0
>From bd6f2fc1073a3e5d9c982ea681094856f67bf30a Mon Sep 17 00:00:00 2001 From: Justin Pryzby <pryz...@telsasoft.com> Date: Thu, 30 Sep 2021 01:22:02 -0500 Subject: [PATCH 2/3] Re: [PATCH] Add extra statistics to explain for Nested Loop Aug 17 Ekaterina Sokol --- src/backend/commands/explain.c | 37 +++++++++++ src/backend/executor/instrument.c | 31 +++++++++ src/include/executor/instrument.h | 6 ++ src/test/regress/expected/explain.out | 10 +++ src/test/regress/expected/partition_prune.out | 66 +++++++++++++++++++ src/test/regress/sql/partition_prune.sql | 7 ++ 6 files changed, 157 insertions(+) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index db99739cc5..428a073099 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -1618,6 +1618,7 @@ ExplainNode(PlanState *planstate, List *ancestors, appendStringInfoString(es->str, " (never executed)"); else { + /* without min and max values because actual result is 0 */ if (es->timing) { ExplainPropertyFloat("Actual Startup Time", "ms", 0.0, 3, es); @@ -3980,6 +3981,11 @@ show_loop_info(Instrumentation *instrument, bool isworker, ExplainState *es) double startup_ms = 1000.0 * instrument->startup / nloops; double total_ms = 1000.0 * instrument->total / nloops; double rows = instrument->ntuples / nloops; + double loop_total_rows = instrument->ntuples; + double loop_min_r = instrument->min_tuples; + double loop_max_r = instrument->max_tuples; + double loop_min_t_ms = 1000.0 * instrument->min_t; + double loop_max_t_ms = 1000.0 * instrument->max_t; if (es->format == EXPLAIN_FORMAT_TEXT) { @@ -3999,6 +4005,20 @@ show_loop_info(Instrumentation *instrument, bool isworker, ExplainState *es) if (!isworker) appendStringInfoChar(es->str, ')'); + + if (nloops > 1 && es->verbose) + { + appendStringInfo(es->str, "\n"); + ExplainIndentText(es); + if (es->timing) + appendStringInfo(es->str, + "Loop min_time: %.3f max_time: %.3f min_rows: %.0f max_rows: %.0f total_rows: %.0f", + loop_min_t_ms, loop_max_t_ms, loop_min_r, loop_max_r, loop_total_rows); + else + appendStringInfo(es->str, + "Loop min_rows: %.0f max_rows: %.0f total_rows: %.0f", + loop_min_r, loop_max_r, loop_total_rows); + } } else { @@ -4009,7 +4029,24 @@ show_loop_info(Instrumentation *instrument, bool isworker, ExplainState *es) ExplainPropertyFloat("Actual Total Time", "ms", total_ms, 3, es); } + + if (nloops > 1 && es->verbose) + { + ExplainPropertyFloat("Loop Min Time", "s", loop_min_t_ms, + 3, es); + ExplainPropertyFloat("Loop Max Time", "s", loop_max_t_ms, + 3, es); + } + ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es); + + if (nloops > 1 && es->verbose) + { + ExplainPropertyFloat("Loop Min Rows", NULL, loop_min_r, 0, es); + ExplainPropertyFloat("Loop Max Rows", NULL, loop_max_r, 0, es); + ExplainPropertyFloat("Loop Total Rows", NULL, loop_total_rows, 0, es); + } + ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es); } } diff --git a/src/backend/executor/instrument.c b/src/backend/executor/instrument.c index 2b106d8473..6588ce2db6 100644 --- a/src/backend/executor/instrument.c +++ b/src/backend/executor/instrument.c @@ -153,7 +153,34 @@ InstrEndLoop(Instrumentation *instr) instr->startup += instr->firsttuple; instr->total += totaltime; + + /* + * this is first loop + * + * We only initialize the min values. We don't need to bother with the + * max, because those are 0 and the non-zero values will get updated a + * couple lines later. + */ + if (instr->nloops == 0) + { + instr->min_t = totaltime; + instr->min_tuples = instr->tuplecount; + } + + if (instr->min_t > totaltime) + instr->min_t = totaltime; + + if (instr->max_t < totaltime) + instr->max_t = totaltime; + instr->ntuples += instr->tuplecount; + + if (instr->min_tuples > instr->tuplecount) + instr->min_tuples = instr->tuplecount; + + if (instr->max_tuples < instr->tuplecount) + instr->max_tuples = instr->tuplecount; + instr->nloops += 1; /* Reset for next cycle (if any) */ @@ -186,6 +213,10 @@ InstrAggNode(Instrumentation *dst, Instrumentation *add) dst->nloops += add->nloops; dst->nfiltered1 += add->nfiltered1; dst->nfiltered2 += add->nfiltered2; + dst->min_t = Min(dst->min_t, add->min_t); + dst->max_t = Max(dst->max_t, add->max_t); + dst->min_tuples = Min(dst->min_tuples, add->min_tuples); + dst->max_tuples = Max(dst->max_tuples, add->max_tuples); /* Add delta of buffer usage since entry to node's totals */ if (dst->need_bufusage) diff --git a/src/include/executor/instrument.h b/src/include/executor/instrument.h index 1f3aa733ec..d0af28b0b8 100644 --- a/src/include/executor/instrument.h +++ b/src/include/executor/instrument.h @@ -79,7 +79,13 @@ typedef struct Instrumentation /* Accumulated statistics across all completed cycles: */ double startup; /* total startup time (in seconds) */ double total; /* total time (in seconds) */ + double min_t; /* time of fastest loop (in seconds) */ + double max_t; /* time of slowest loop (in seconds) */ double ntuples; /* total tuples produced */ + double min_tuples; /* min counter of produced tuples for all + * loops */ + double max_tuples; /* max counter of produced tuples for all + * loops */ double ntuples2; /* secondary node-specific tuple counter */ double nloops; /* # of run cycles for this node */ double nfiltered1; /* # tuples removed by scanqual or joinqual OR diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out index 1734dfee8c..275cdb3767 100644 --- a/src/test/regress/expected/explain.out +++ b/src/test/regress/expected/explain.out @@ -354,8 +354,13 @@ select jsonb_pretty( "Actual Loops": 0, + "Startup Cost": 0.0, + "Async Capable": false, + + "Loop Max Rows": 0, + + "Loop Max Time": 0.0, + + "Loop Min Rows": 0, + + "Loop Min Time": 0.0, + "Relation Name": "tenk1", + "Parallel Aware": true, + + "Loop Total Rows": 0, + "Local Hit Blocks": 0, + "Temp Read Blocks": 0, + "Actual Total Time": 0.0, + @@ -400,7 +405,12 @@ select jsonb_pretty( "Actual Loops": 0, + "Startup Cost": 0.0, + "Async Capable": false, + + "Loop Max Rows": 0, + + "Loop Max Time": 0.0, + + "Loop Min Rows": 0, + + "Loop Min Time": 0.0, + "Parallel Aware": false, + + "Loop Total Rows": 0, + "Sort Space Used": 0, + "Local Hit Blocks": 0, + "Temp Read Blocks": 0, + diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index 7555764c77..0e1242c089 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -2715,6 +2715,72 @@ order by tbl1.col1, tprt.col1; 1001 | 1001 (3 rows) +-- Tests for extra statistics +explain (analyze, verbose, costs off, summary off, timing off) +select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1; + QUERY PLAN +--------------------------------------------------------------------------------- + Nested Loop (actual rows=23 loops=1) + Output: tbl1.col1, tprt.col1 + -> Seq Scan on public.tbl1 (actual rows=5 loops=1) + Output: tbl1.col1 + -> Append (actual rows=5 loops=5) + Loop min_rows: 2 max_rows: 6 total_rows: 23 + -> Index Scan using tprt1_idx on public.tprt_1 (actual rows=2 loops=5) + Loop min_rows: 2 max_rows: 2 total_rows: 10 + Output: tprt_1.col1 + Index Cond: (tprt_1.col1 < tbl1.col1) + -> Index Scan using tprt2_idx on public.tprt_2 (actual rows=3 loops=4) + Loop min_rows: 2 max_rows: 3 total_rows: 11 + Output: tprt_2.col1 + Index Cond: (tprt_2.col1 < tbl1.col1) + -> Index Scan using tprt3_idx on public.tprt_3 (actual rows=1 loops=2) + Loop min_rows: 1 max_rows: 1 total_rows: 2 + Output: tprt_3.col1 + Index Cond: (tprt_3.col1 < tbl1.col1) + -> Index Scan using tprt4_idx on public.tprt_4 (never executed) + Output: tprt_4.col1 + Index Cond: (tprt_4.col1 < tbl1.col1) + -> Index Scan using tprt5_idx on public.tprt_5 (never executed) + Output: tprt_5.col1 + Index Cond: (tprt_5.col1 < tbl1.col1) + -> Index Scan using tprt6_idx on public.tprt_6 (never executed) + Output: tprt_6.col1 + Index Cond: (tprt_6.col1 < tbl1.col1) +(27 rows) + +explain (analyze, verbose, costs off, summary off, timing off) +select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1; + QUERY PLAN +--------------------------------------------------------------------------------- + Nested Loop (actual rows=3 loops=1) + Output: tbl1.col1, tprt.col1 + -> Seq Scan on public.tbl1 (actual rows=5 loops=1) + Output: tbl1.col1 + -> Append (actual rows=1 loops=5) + Loop min_rows: 0 max_rows: 1 total_rows: 3 + -> Index Scan using tprt1_idx on public.tprt_1 (never executed) + Output: tprt_1.col1 + Index Cond: (tprt_1.col1 = tbl1.col1) + -> Index Scan using tprt2_idx on public.tprt_2 (actual rows=1 loops=2) + Loop min_rows: 1 max_rows: 1 total_rows: 2 + Output: tprt_2.col1 + Index Cond: (tprt_2.col1 = tbl1.col1) + -> Index Scan using tprt3_idx on public.tprt_3 (actual rows=0 loops=3) + Loop min_rows: 0 max_rows: 1 total_rows: 1 + Output: tprt_3.col1 + Index Cond: (tprt_3.col1 = tbl1.col1) + -> Index Scan using tprt4_idx on public.tprt_4 (never executed) + Output: tprt_4.col1 + Index Cond: (tprt_4.col1 = tbl1.col1) + -> Index Scan using tprt5_idx on public.tprt_5 (never executed) + Output: tprt_5.col1 + Index Cond: (tprt_5.col1 = tbl1.col1) + -> Index Scan using tprt6_idx on public.tprt_6 (never executed) + Output: tprt_6.col1 + Index Cond: (tprt_6.col1 = tbl1.col1) +(26 rows) + -- Last partition delete from tbl1; insert into tbl1 values (4400); diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql index d70bd8610c..d5ba8d6f38 100644 --- a/src/test/regress/sql/partition_prune.sql +++ b/src/test/regress/sql/partition_prune.sql @@ -654,6 +654,13 @@ select tbl1.col1, tprt.col1 from tbl1 inner join tprt on tbl1.col1 = tprt.col1 order by tbl1.col1, tprt.col1; +-- Tests for extra statistics +explain (analyze, verbose, costs off, summary off, timing off) +select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1; + +explain (analyze, verbose, costs off, summary off, timing off) +select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1; + -- Last partition delete from tbl1; insert into tbl1 values (4400); -- 2.17.0
>From b7c5c53a210ecf1895e70d206f1d67a9e4f13163 Mon Sep 17 00:00:00 2001 From: Justin Pryzby <pryz...@telsasoft.com> Date: Thu, 30 Sep 2021 01:58:11 -0500 Subject: [PATCH 3/3] Justin's changes --- src/backend/commands/explain.c | 24 +++++++++---------- src/test/regress/expected/partition_prune.out | 14 +++++------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 428a073099..756051864c 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -4008,15 +4008,16 @@ show_loop_info(Instrumentation *instrument, bool isworker, ExplainState *es) if (nloops > 1 && es->verbose) { - appendStringInfo(es->str, "\n"); + appendStringInfoChar(es->str, '\n'); ExplainIndentText(es); + if (es->timing) appendStringInfo(es->str, - "Loop min_time: %.3f max_time: %.3f min_rows: %.0f max_rows: %.0f total_rows: %.0f", - loop_min_t_ms, loop_max_t_ms, loop_min_r, loop_max_r, loop_total_rows); + "Loop Min Rows: %.0f Max Rows: %.0f Total Rows: %.0f Min Time: %.3f Max Time: %.3f", + loop_min_r, loop_max_r, loop_total_rows, loop_min_t_ms, loop_max_t_ms); else appendStringInfo(es->str, - "Loop min_rows: %.0f max_rows: %.0f total_rows: %.0f", + "Loop Min Rows: %.0f Max Rows: %.0f Total Rows: %.0f", loop_min_r, loop_max_r, loop_total_rows); } } @@ -4028,14 +4029,13 @@ show_loop_info(Instrumentation *instrument, bool isworker, ExplainState *es) 3, es); ExplainPropertyFloat("Actual Total Time", "ms", total_ms, 3, es); - } - - if (nloops > 1 && es->verbose) - { - ExplainPropertyFloat("Loop Min Time", "s", loop_min_t_ms, - 3, es); - ExplainPropertyFloat("Loop Max Time", "s", loop_max_t_ms, - 3, es); + if (nloops > 1 && es->verbose) + { + ExplainPropertyFloat("Loop Min Time", "ms", loop_min_t_ms, + 3, es); + ExplainPropertyFloat("Loop Max Time", "ms", loop_max_t_ms, + 3, es); + } } ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es); diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index 0e1242c089..d9d77090dc 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -2725,17 +2725,17 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1; -> Seq Scan on public.tbl1 (actual rows=5 loops=1) Output: tbl1.col1 -> Append (actual rows=5 loops=5) - Loop min_rows: 2 max_rows: 6 total_rows: 23 + Loop Min Rows: 2 Max Rows: 6 Total Rows: 23 -> Index Scan using tprt1_idx on public.tprt_1 (actual rows=2 loops=5) - Loop min_rows: 2 max_rows: 2 total_rows: 10 + Loop Min Rows: 2 Max Rows: 2 Total Rows: 10 Output: tprt_1.col1 Index Cond: (tprt_1.col1 < tbl1.col1) -> Index Scan using tprt2_idx on public.tprt_2 (actual rows=3 loops=4) - Loop min_rows: 2 max_rows: 3 total_rows: 11 + Loop Min Rows: 2 Max Rows: 3 Total Rows: 11 Output: tprt_2.col1 Index Cond: (tprt_2.col1 < tbl1.col1) -> Index Scan using tprt3_idx on public.tprt_3 (actual rows=1 loops=2) - Loop min_rows: 1 max_rows: 1 total_rows: 2 + Loop Min Rows: 1 Max Rows: 1 Total Rows: 2 Output: tprt_3.col1 Index Cond: (tprt_3.col1 < tbl1.col1) -> Index Scan using tprt4_idx on public.tprt_4 (never executed) @@ -2758,16 +2758,16 @@ select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1; -> Seq Scan on public.tbl1 (actual rows=5 loops=1) Output: tbl1.col1 -> Append (actual rows=1 loops=5) - Loop min_rows: 0 max_rows: 1 total_rows: 3 + Loop Min Rows: 0 Max Rows: 1 Total Rows: 3 -> Index Scan using tprt1_idx on public.tprt_1 (never executed) Output: tprt_1.col1 Index Cond: (tprt_1.col1 = tbl1.col1) -> Index Scan using tprt2_idx on public.tprt_2 (actual rows=1 loops=2) - Loop min_rows: 1 max_rows: 1 total_rows: 2 + Loop Min Rows: 1 Max Rows: 1 Total Rows: 2 Output: tprt_2.col1 Index Cond: (tprt_2.col1 = tbl1.col1) -> Index Scan using tprt3_idx on public.tprt_3 (actual rows=0 loops=3) - Loop min_rows: 0 max_rows: 1 total_rows: 1 + Loop Min Rows: 0 Max Rows: 1 Total Rows: 1 Output: tprt_3.col1 Index Cond: (tprt_3.col1 = tbl1.col1) -> Index Scan using tprt4_idx on public.tprt_4 (never executed) -- 2.17.0