explain analyze rows=%.0f
I have always assumed that there is some very good reason why EXPLAIN
ANALYZE reports the number of rows as an integer rather than a
floating point value, but in reading explain.c it seems that the
reason is just that we decided to round to zero decimal places. Any
chance we could reconsider this decision? I often find myself wanting
to know the value that is here called ntuples, but rounding
ntuples/nloops off to the nearest integer loses too much precision.
(Before someone mentions it, yes that would be a good thing to include
in XML-formatted explain output. But I don't see that including a
couple of decimal places would hurt the text output format either.)
...Robert
Robert Haas escreveu:
I have always assumed that there is some very good reason why EXPLAIN
ANALYZE reports the number of rows as an integer rather than a
floating point value, but in reading explain.c it seems that the
reason is just that we decided to round to zero decimal places. Any
chance we could reconsider this decision? I often find myself wanting
to know the value that is here called ntuples, but rounding
ntuples/nloops off to the nearest integer loses too much precision.
Don't you think is too strange having, for example, 6.67 rows? I would confuse
users and programs that parses the EXPLAIN output. However, I wouldn't object
to add ntuples to an extended explain output (as discussed in the other thread).
--
Euler Taveira de Oliveira
http://www.timbira.com/
On Thu, May 28, 2009 at 11:00 PM, Euler Taveira de Oliveira
<euler@timbira.com> wrote:
Robert Haas escreveu:
I have always assumed that there is some very good reason why EXPLAIN
ANALYZE reports the number of rows as an integer rather than a
floating point value, but in reading explain.c it seems that the
reason is just that we decided to round to zero decimal places. Any
chance we could reconsider this decision? I often find myself wanting
to know the value that is here called ntuples, but rounding
ntuples/nloops off to the nearest integer loses too much precision.Don't you think is too strange having, for example, 6.67 rows?
No stranger than having it say 7 when it's really not. Actually mine
mostly come out 1 when the real value is somewhere between 0.5 and
1.49. :-(
...Robert
On Thu, May 28, 2009 at 11:12:42PM -0400, Robert Haas wrote:
On Thu, May 28, 2009 at 11:00 PM, Euler Taveira de Oliveira
Don't you think is too strange having, for example, 6.67 rows?
No stranger than having it say 7 when it's really not. Actually mine
mostly come out 1 when the real value is somewhere between 0.5 and
1.49. :-(
+1. It would help users realize more quickly that some of the values in the
EXPLAIN output are, for instance, *average* number of rows *per iteration* of a
nested loop, say, rather than total rows found in all loops. That's an
important distinction that isn't immediately clear to the novice EXPLAIN
reader, but would become so very quickly as users tried to figure out how a
scan could come up with a fractional row.
- Josh / eggyknap
Joshua Tolley <eggyknap@gmail.com> writes:
On Thu, May 28, 2009 at 11:12:42PM -0400, Robert Haas wrote:
On Thu, May 28, 2009 at 11:00 PM, Euler Taveira de Oliveira
Don't you think is too strange having, for example, 6.67 rows?
No stranger than having it say 7 when it's really not. Actually mine
mostly come out 1 when the real value is somewhere between 0.5 and
1.49. :-(
+1. It would help users realize more quickly that some of the values in the
EXPLAIN output are, for instance, *average* number of rows *per iteration* of a
nested loop, say, rather than total rows found in all loops.
I think it would only be sensible to show fractional digits if nloops is
greater than 1. Otherwise the value must in fact be an integer, and
you're just going to confuse people more by suggesting that it might not
be.
regards, tom lane
On Fri, May 29, 2009 at 1:30 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Joshua Tolley <eggyknap@gmail.com> writes:
On Thu, May 28, 2009 at 11:12:42PM -0400, Robert Haas wrote:
On Thu, May 28, 2009 at 11:00 PM, Euler Taveira de Oliveira
Don't you think is too strange having, for example, 6.67 rows?
No stranger than having it say 7 when it's really not. Actually mine
mostly come out 1 when the real value is somewhere between 0.5 and
1.49. :-(+1. It would help users realize more quickly that some of the values in the
EXPLAIN output are, for instance, *average* number of rows *per iteration* of a
nested loop, say, rather than total rows found in all loops.I think it would only be sensible to show fractional digits if nloops is
greater than 1. Otherwise the value must in fact be an integer, and
you're just going to confuse people more by suggesting that it might not
be.
That might be over-engineering, but I'll take it.
...Robert
Euler Taveira de Oliveira wrote:
Robert Haas escreveu:
...EXPLAIN ANALYZE reports the number of rows as an integer... Any
chance we could reconsider this decision? I often find myself wanting
to know the value that is here called ntuples, but rounding
ntuples/nloops off to the nearest integer loses too much precision.Don't you think is too strange having, for example, 6.67 rows? I would confuse
users and programs that parses the EXPLAIN output. However, I wouldn't object
I don't think it's that confusing. If it says "0.1 rows", I imagine most
people would infer that this means "typically 0, but sometimes 1 or a few" rows.
What I'd find strange about "6.67 rows" in your example is more that on
the estimated rows side, it seems to imply an unrealistically precise estimate
in the same way that "667 rows" would seem unrealistically precise to me.
Maybe rounding to 2 significant digits would reduce confusion?
On Mon, 2009-06-01 at 20:30 -0700, Ron Mayer wrote:
What I'd find strange about "6.67 rows" in your example is more that on
the estimated rows side, it seems to imply an unrealistically precise estimate
in the same way that "667 rows" would seem unrealistically precise to me.
Maybe rounding to 2 significant digits would reduce confusion?
You're right that the number of significant digits already exceeds the
true accuracy of the computation. I think what Robert wants to see is
the exact value used in the calc, so the estimates can be checked more
thoroughly than is currently possible.
--
Simon Riggs www.2ndQuadrant.com
PostgreSQL Training, Services and Support
On Jun 2, 2009, at 9:41 AM, Simon Riggs <simon@2ndQuadrant.com> wrote:
On Mon, 2009-06-01 at 20:30 -0700, Ron Mayer wrote:
What I'd find strange about "6.67 rows" in your example is more
that on
the estimated rows side, it seems to imply an unrealistically
precise estimate
in the same way that "667 rows" would seem unrealistically precise
to me.
Maybe rounding to 2 significant digits would reduce confusion?You're right that the number of significant digits already exceeds the
true accuracy of the computation. I think what Robert wants to see is
the exact value used in the calc, so the estimates can be checked more
thoroughly than is currently possible.
Bingo.
...Robert
Robert Haas <robertmhaas@gmail.com> writes:
On Jun 2, 2009, at 9:41 AM, Simon Riggs <simon@2ndQuadrant.com> wrote:
You're right that the number of significant digits already exceeds the
true accuracy of the computation. I think what Robert wants to see is
the exact value used in the calc, so the estimates can be checked more
thoroughly than is currently possible.
Bingo.
Uh, the planner's estimate *is* an integer. What was under discussion
(I thought) was showing some fractional digits in the case where EXPLAIN
ANALYZE is outputting a measured row count that is an average over
multiple loops, and therefore isn't necessarily an integer. In that
case the measured value can be considered arbitrarily precise --- though
I think in practice one or two fractional digits would be plenty.
regards, tom lane
...Robert
On Jun 2, 2009, at 10:38 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Jun 2, 2009, at 9:41 AM, Simon Riggs <simon@2ndQuadrant.com>
wrote:You're right that the number of significant digits already exceeds
the
true accuracy of the computation. I think what Robert wants to see
is
the exact value used in the calc, so the estimates can be checked
more
thoroughly than is currently possible.Bingo.
Uh, the planner's estimate *is* an integer. What was under discussion
(I thought) was showing some fractional digits in the case where
EXPLAIN
ANALYZE is outputting a measured row count that is an average over
multiple loops, and therefore isn't necessarily an integer. In that
case the measured value can be considered arbitrarily precise ---
though
I think in practice one or two fractional digits would be plenty.
We're in violent agreement here.
...Robert
On Thu, Jun 23, 2022 at 12:01 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Jun 2, 2009, at 9:41 AM, Simon Riggs <simon@2ndQuadrant.com> wrote:
You're right that the number of significant digits already exceeds the
true accuracy of the computation. I think what Robert wants to see is
the exact value used in the calc, so the estimates can be checked more
thoroughly than is currently possible.Bingo.
Uh, the planner's estimate *is* an integer. What was under discussion
(I thought) was showing some fractional digits in the case where EXPLAIN
ANALYZE is outputting a measured row count that is an average over
multiple loops, and therefore isn't necessarily an integer. In that
case the measured value can be considered arbitrarily precise --- though
I think in practice one or two fractional digits would be plenty.regards, tom lane
Hi,
I was looking at the TODO list and found that the issue requires
a quick fix. Attached is a patch which shows output like this. It shows the
fraction digits in case of loops > 1
postgres=# explain analyze select * from foo;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------
Seq Scan on foo (cost=0.00..64414.79 rows=2326379 width=8) (actual
time=0.025..277.096 rows=2344671 loops=1
Planning Time: 0.516 ms
Execution Time: 356.993 ms
(3 rows)
postgres=# explain analyze select * from foo where b = (select c from
bar where c = 1);
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
Seq Scan on foo (cost=8094.37..78325.11 rows=2326379 width=8)
(actual time=72.352..519.159 rows=2344671 loops=1
Filter: (b = $1)
InitPlan 1 (returns $1)
-> Gather (cost=1000.00..8094.37 rows=1 width=4) (actual
time=0.872..72.434 rows=1 loops=1
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on bar (cost=0.00..7094.27 rows=1
width=4) (actual time=41.931..65.382 rows=0.33 loops=3)
Filter: (c = 1)
Rows Removed by Filter: 245457
Planning Time: 0.277 ms
Execution Time: 597.795 ms
(11 rows)
--
Ibrar Ahmed
Attachments:
explain_float_row.patchapplication/octet-stream; name=explain_float_row.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5d1f7089da..954f64b802 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1624,13 +1624,21 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
if (es->timing)
+ {
+ appendStringInfo(es->str, " (actual time=%.3f..%.3f",
+ startup_ms, total_ms);
appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ nloops == 1 ? " rows=%.0f loops=%.0f" :
+ " rows=%.2f loops=%.0f)",
+ rows, nloops);
+ }
else
+ {
appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ nloops == 1 ?
+ " (actual rows=%.0f loops=%.0f": " rows=%.2f loops=%.0f)",
+ rows, nloops);
+ }
}
else
{
@@ -1641,7 +1649,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Rows", NULL, rows, nloops == 1 ? 0 : 2, es);
ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
}
}
@@ -1690,13 +1698,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
{
ExplainIndentText(es);
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ {
+ appendStringInfo(es->str, "actual time=%.3f..%.3f",
+ startup_ms, total_ms);
+ appendStringInfo(es->str,
+ nloops == 1 ? " rows=%.0f loops=%.0f" :
+ " rows=%.2f loops=%.0f",
+ rows, nloops);
+ }
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ {
+ appendStringInfo(es->str, nloops == 1 ?
+ "actual rows=%.0f loops=%.0f": " rows=%.2f loops=%.0f",
+ rows, nloops);
+ }
}
else
{
@@ -1707,7 +1722,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Rows", NULL, rows, nloops == 1 ? 0 : 2, es);
ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
}
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index ce12915592..baecc40e0c 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1937,7 +1937,7 @@ _outPathInfo(StringInfo str, const Path *node)
WRITE_BOOL_FIELD(parallel_aware);
WRITE_BOOL_FIELD(parallel_safe);
WRITE_INT_FIELD(parallel_workers);
- WRITE_FLOAT_FIELD(rows, "%.0f");
+ WRITE_FLOAT_FIELD(rows, "%.2f");
WRITE_FLOAT_FIELD(startup_cost, "%.2f");
WRITE_FLOAT_FIELD(total_cost, "%.2f");
WRITE_NODE_FIELD(pathkeys);
On Wed, Jun 22, 2022 at 12:11 PM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Thu, Jun 23, 2022 at 12:01 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Jun 2, 2009, at 9:41 AM, Simon Riggs <simon@2ndQuadrant.com> wrote:
You're right that the number of significant digits already exceeds the
true accuracy of the computation. I think what Robert wants to see is
the exact value used in the calc, so the estimates can be checked more
thoroughly than is currently possible.Bingo.
Uh, the planner's estimate *is* an integer. What was under discussion
(I thought) was showing some fractional digits in the case where EXPLAIN
ANALYZE is outputting a measured row count that is an average over
multiple loops, and therefore isn't necessarily an integer. In that
case the measured value can be considered arbitrarily precise --- though
I think in practice one or two fractional digits would be plenty.regards, tom lane
Hi,
I was looking at the TODO list and found that the issue requires
a quick fix. Attached is a patch which shows output like this.
Quick code review:
+ "actual rows=%.0f loops=%.0f": " rows=%.2f loops=%.0f",
The leading space before the else block "rows" does not belong.
There should be a space after the colon.
The word "actual" that you are dropping in the else block seems like it
should belong - it is a header for the entire section not just a modifier
for the word "rows". This is evidenced by the timing block verbiage where
rows is standalone and the word actual comes before time. In short, only
the format specifier should change under the current scheme. Both sections.
- WRITE_FLOAT_FIELD(rows, "%.0f");
+ WRITE_FLOAT_FIELD(rows, "%.2f");
This one looks suspicious, though I haven't dug into the code to see
exactly what all is being touched. That it doesn't have an nloops
condition like everything else stands out.
Tooling that expects an integer is the only downside I see here, but I
concur that the usability of always showing two decimal places when nloops
1 overcomes any objection I have on those grounds.
David J.
On Thu, Jun 23, 2022 at 1:04 AM David G. Johnston <
david.g.johnston@gmail.com> wrote:
On Wed, Jun 22, 2022 at 12:11 PM Ibrar Ahmed <ibrar.ahmad@gmail.com>
wrote:On Thu, Jun 23, 2022 at 12:01 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Jun 2, 2009, at 9:41 AM, Simon Riggs <simon@2ndQuadrant.com> wrote:
You're right that the number of significant digits already exceeds the
true accuracy of the computation. I think what Robert wants to see is
the exact value used in the calc, so the estimates can be checked more
thoroughly than is currently possible.Bingo.
Uh, the planner's estimate *is* an integer. What was under discussion
(I thought) was showing some fractional digits in the case where EXPLAIN
ANALYZE is outputting a measured row count that is an average over
multiple loops, and therefore isn't necessarily an integer. In that
case the measured value can be considered arbitrarily precise --- though
I think in practice one or two fractional digits would be plenty.regards, tom lane
Hi,
I was looking at the TODO list and found that the issue requires
a quick fix. Attached is a patch which shows output like this.Quick code review:
+ "actual rows=%.0f loops=%.0f": " rows=%.2f loops=%.0f",
The leading space before the else block "rows" does not belong.
There should be a space after the colon.
Thanks, David for your quick response. I have updated the patch.
The word "actual" that you are dropping in the else block seems like it
should belong - it is a header for the entire section not just a modifier
for the word "rows". This is evidenced by the timing block verbiage where
rows is standalone and the word actual comes before time. In short, only
the format specifier should change under the current scheme. Both sections.- WRITE_FLOAT_FIELD(rows, "%.0f"); + WRITE_FLOAT_FIELD(rows, "%.2f");This one looks suspicious, though I haven't dug into the code to see
exactly what all is being touched. That it doesn't have an nloops
condition like everything else stands out.I was also thinking about that, but I don't see any harm when we
ultimately truncating that decimal
at a latter stage of code in case of loop = 1.
Tooling that expects an integer is the only downside I see here, but I
concur that the usability of always showing two decimal places when nloops1 overcomes any objection I have on those grounds.
David J.
--
Ibrar Ahmed
Attachments:
explain_float_row_v2.patchapplication/octet-stream; name=explain_float_row_v2.patchDownload
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 47d80b0d25..bd60b55574 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -122,7 +122,7 @@ bool bsysscan = false;
* lookups as fast as possible.
*/
static FullTransactionId XactTopFullTransactionId = {InvalidTransactionId};
-static int nParallelCurrentXids = 0;
+static int nParallelCurrentXids = 0;
static TransactionId *ParallelCurrentXids;
/*
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5d1f7089da..d53c9c7235 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1624,13 +1624,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
if (es->timing)
+ {
+ appendStringInfo(es->str, " (actual time=%.3f..%.3f",
+ startup_ms, total_ms);
appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ nloops == 1 ? " rows=%.0f loops=%.0f)" : " rows=%.2f loops=%.0f)",
+ rows, nloops);
+ }
else
+ {
appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
+ nloops == 1 ?
+ " (actual rows=%.0f loops=%.0f)" : " (actual rows=%.2f loops=%.0f)",
rows, nloops);
+ }
}
else
{
@@ -1641,7 +1648,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Rows", NULL, rows, nloops == 1 ? 0 : 2, es);
ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
}
}
@@ -1690,13 +1697,19 @@ ExplainNode(PlanState *planstate, List *ancestors,
{
ExplainIndentText(es);
if (es->timing)
+ {
+ appendStringInfo(es->str, "actual time=%.3f..%.3f",
+ startup_ms, total_ms);
appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ nloops == 1 ? " rows=%.0f loops=%.0f" : " rows=%.2f loops=%.0f",
+ rows, nloops);
+ }
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
+ {
+ appendStringInfo(es->str, nloops == 1 ?
+ "actual rows=%.0f loops=%.0f" : "actual rows=%.2f loops=%.0f",
rows, nloops);
+ }
}
else
{
@@ -1707,7 +1720,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Rows", NULL, rows, nloops == 1 ? 0 : 2, es);
ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
}
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index ce12915592..baecc40e0c 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1937,7 +1937,7 @@ _outPathInfo(StringInfo str, const Path *node)
WRITE_BOOL_FIELD(parallel_aware);
WRITE_BOOL_FIELD(parallel_safe);
WRITE_INT_FIELD(parallel_workers);
- WRITE_FLOAT_FIELD(rows, "%.0f");
+ WRITE_FLOAT_FIELD(rows, "%.2f");
WRITE_FLOAT_FIELD(startup_cost, "%.2f");
WRITE_FLOAT_FIELD(total_cost, "%.2f");
WRITE_NODE_FIELD(pathkeys);
On Thu, Jun 23, 2022 at 2:25 AM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Thu, Jun 23, 2022 at 1:04 AM David G. Johnston <david.g.johnston@gmail.com> wrote:
- WRITE_FLOAT_FIELD(rows, "%.0f"); + WRITE_FLOAT_FIELD(rows, "%.2f");This one looks suspicious, though I haven't dug into the code to see exactly what all is being touched. That it doesn't have an nloops condition like everything else stands out.
I was also thinking about that, but I don't see any harm when we ultimately truncating that decimal
at a latter stage of code in case of loop = 1.
That change is in the path node which we anyway not going to target as
part of this change. We only want to change the display for actual
rows in Explain Analyze. So, I can't see how the quoted change can
help in any way.
Few miscellaneous comments:
========================
*
static FullTransactionId XactTopFullTransactionId = {InvalidTransactionId};
-static int nParallelCurrentXids = 0;
+static int nParallelCurrentXids = 0;
I don't see why this change is required.
* Can you please add a comment explaining why we are making this
change for actual rows?
* Can you please write a test case unless there is some existing test
that covers the change by displaying actual rows values in decimal but
in that case patch should have that changed output test? If you don't
think we can reliably write such a test then please let me know the
reason?
--
With Regards,
Amit Kapila.
On Thu, Jun 23, 2022 at 2:25 AM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Thu, Jun 23, 2022 at 1:04 AM David G. Johnston <david.g.johnston@gmail.com> wrote:
On Wed, Jun 22, 2022 at 12:11 PM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Thu, Jun 23, 2022 at 12:01 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Jun 2, 2009, at 9:41 AM, Simon Riggs <simon@2ndQuadrant.com> wrote:
You're right that the number of significant digits already exceeds the
true accuracy of the computation. I think what Robert wants to see is
the exact value used in the calc, so the estimates can be checked more
thoroughly than is currently possible.Bingo.
Uh, the planner's estimate *is* an integer. What was under discussion
(I thought) was showing some fractional digits in the case where EXPLAIN
ANALYZE is outputting a measured row count that is an average over
multiple loops, and therefore isn't necessarily an integer. In that
case the measured value can be considered arbitrarily precise --- though
I think in practice one or two fractional digits would be plenty.regards, tom lane
Hi,
I was looking at the TODO list and found that the issue requires
a quick fix. Attached is a patch which shows output like this.Quick code review:
+ "actual rows=%.0f loops=%.0f": " rows=%.2f loops=%.0f",
The leading space before the else block "rows" does not belong.
There should be a space after the colon.
Thanks, David for your quick response. I have updated the patch.
The word "actual" that you are dropping in the else block seems like it should belong - it is a header for the entire section not just a modifier for the word "rows". This is evidenced by the timing block verbiage where rows is standalone and the word actual comes before time. In short, only the format specifier should change under the current scheme. Both sections.
- WRITE_FLOAT_FIELD(rows, "%.0f"); + WRITE_FLOAT_FIELD(rows, "%.2f");This one looks suspicious, though I haven't dug into the code to see exactly what all is being touched. That it doesn't have an nloops condition like everything else stands out.
I was also thinking about that, but I don't see any harm when we ultimately truncating that decimal
at a latter stage of code in case of loop = 1.
Thanks for the patch.
1) There are some existing regression tests that are failing, you
should update the expect files accordingly for the same:
--- /home/vignesh/postgres/src/test/regress/expected/select_parallel.out
2022-05-18 20:51:46.874818044 +0530
+++ /home/vignesh/postgres/src/test/regress/results/select_parallel.out
2022-07-07 15:27:34.450440922 +0530
@@ -545,17 +545,17 @@
explain (analyze, timing off, summary off, costs off)
select count(*) from tenk1, tenk2 where tenk1.hundred > 1
and tenk2.thousand=0;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Aggregate (actual rows=1 loops=1)
-> Nested Loop (actual rows=98000 loops=1)
-> Seq Scan on tenk2 (actual rows=10 loops=1)
Filter: (thousand = 0)
Rows Removed by Filter: 9990
- -> Gather (actual rows=9800 loops=10)
+ -> Gather (actual rows=9800.00 loops=10)
Workers Planned: 4
Workers Launched: 4
- -> Parallel Seq Scan on tenk1 (actual rows=1960 loops=50)
+ -> Parallel Seq Scan on tenk1 (actual rows=1960.00 loops=50)
Filter: (hundred > 1)
test select_parallel ... FAILED 744 ms
partition_prune ... FAILED 861 ms
explain ... FAILED 134 ms
memoize ... FAILED 250 ms
2) This change is not required as part of this patch:
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -122,7 +122,7 @@ bool bsysscan = false;
* lookups as fast as possible.
*/
static FullTransactionId XactTopFullTransactionId = {InvalidTransactionId};
-static int nParallelCurrentXids = 0;
+static int nParallelCurrentXids = 0;
static TransactionId *ParallelCurrentXids;
Regards,
Vignesh
- -> Parallel Seq Scan on tenk1 (actual rows=1960 loops=50) + -> Parallel Seq Scan on tenk1 (actual rows=1960.00
At the not inconsiderable risk of bike-shedding....
I'm wondering if printing something like 0.00 will be somewhat
deceptive when the real value is non-zero but less than 1 row per 200
loops. I wonder if the number of decimal places should be calculated
to produce a minimum of one non-zero digit for non-zero values.
--
greg
On Thu, Jul 7, 2022 at 1:53 PM Greg Stark <stark@mit.edu> wrote:
- -> Parallel Seq Scan on tenk1 (actual rows=1960 loops=50) + -> Parallel Seq Scan on tenk1 (actual rows=1960.00At the not inconsiderable risk of bike-shedding....
I'm wondering if printing something like 0.00 will be somewhat
deceptive when the real value is non-zero but less than 1 row per 200
loops. I wonder if the number of decimal places should be calculated
to produce a minimum of one non-zero digit for non-zero values.
I mean, what I really want here if I'm honest is to not have the
system divide the number of rows by the loop count. And it sort of
sounds like maybe that's what you want, too. You want to know whether
the loop count is actually zero, not whether it's close to zero when
you divide it by some number that might be gigantic.
Parallel query's treatment of this topic has come in for some
criticism, but I don't know what else it could really do: there could
be any number of loops in each backend, and it need not be the same
across all backends, so all it can do is add up the loop counts just
like it adds up the row counts and times. And if we just printed out
those totals, the result would be understandable by everyone. But we
insist on dividing it by the loop count, and then things get really
obscure. Consider this example, which does not involve parallel query:
Nested Loop (actual time=TIME FOR THIS AND ALL CHILDREN rows=THE REAL
ROW COUNT loops=1)
-> Seq Scan on something (actual time=THE TIME IT REALLY TOOK rows=THE
REAL ROW COUNT loops=1)
-> Index Scan using someidx on somethingelse (actual time=NOT REALLY
HOW LONG IT TOOK rows=NOT REALLY HOW MANY ROWS WE GOT loops=HUGE
NUMBER)
If I'm looking at this plan and trying to find out what's gone wrong,
I want to know how much time got spent in the nested loop, how much
time got spent in the Seq Scan, and how much time got spent in the
Index Scan. It's easy to figure out how much time got spent in the Seq
Scan, but to find out how much time got spent in the Index Scan, I
have to multiply the time by the loop count. Then, I have to add that
number to the time spent in the Seq Scan and subtract that from the
time from the nested loop to find the time spent on the nested loop
itself. This is quite a lot of computation, especially if the plan
involves a dozen or two different nested loops, and if we didn't
insist on dividing the time by the loop count, it would be MUCH EASIER
to figure out whether the time spent in the Index Scan is a
significant percentage of the total time or not.
And likewise, if you're trying to understand the row count for the
nested loop, it would be a heck of a lot simpler if you could see the
*raw* row count for the index scan. It's unclear to me what value
there ever is in knowing that the number of rows per iteration was
about 0 or about 1 or about 2. The only thing I'm ever going to do
with the row count that gets printed here is multiply it by the loop
count and then try to figure out how much precision I've lost because
of limits on the number of decimal places. Right now that's basically
all of it because nearly every case ends up with the index scan having
rows=1, so even just adding 2 decimal places will help a lot. But I'm
still just going to be reverse engineering what I really want to know,
which is the original number, from what the system gives me, which is
a needlessly-obfuscated version of that value.
Grumble, grumble. It's sad that it's been 13 years and we haven't done
anything about this.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Jul 7, 2022 at 1:21 PM Robert Haas <robertmhaas@gmail.com> wrote:
Nested Loop (actual time=TIME FOR THIS AND ALL CHILDREN rows=THE REAL
ROW COUNT loops=1)
-> Seq Scan on something (actual time=THE TIME IT REALLY TOOK rows=THE
REAL ROW COUNT loops=1)
-> Index Scan using someidx on somethingelse (actual time=NOT REALLY
HOW LONG IT TOOK rows=NOT REALLY HOW MANY ROWS WE GOT loops=HUGE
NUMBER)If I'm looking at this plan and trying to find out what's gone wrong,
I want to know how much time got spent in the nested loop, how much
time got spent in the Seq Scan, and how much time got spent in the
Index Scan. It's easy to figure out how much time got spent in the Seq
Scan, but to find out how much time got spent in the Index Scan, I
have to multiply the time by the loop count.
I agree that this general state of affairs is very confusing, and
seems like something that we should still improve. Just because it's a
very old way of presenting the information doesn't mean that it's the
best one, or even a particularly good one. Plus you could probably
make some kind of concession in the direction of maintaining
compatibility with the current approach if you had to. Right?
--
Peter Geoghegan
On Thu, Jul 07, 2022 at 04:21:37PM -0400, Robert Haas wrote:
I mean, what I really want here if I'm honest is to not have the
system divide the number of rows by the loop count. And it sort of
sounds like maybe that's what you want, too. You want to know whether
the loop count is actually zero, not whether it's close to zero when
you divide it by some number that might be gigantic.
...
involves a dozen or two different nested loops, and if we didn't
insist on dividing the time by the loop count, it would be MUCH EASIER
to figure out whether the time spent in the Index Scan is a
significant percentage of the total time or not.
I think the guiding princible for what to do should be to reduce how much is
needed to explain about how to interpret what explain is showing...
The docs say this:
| In such cases, the loops value reports the total number of executions of the
| node, and the actual time and rows values shown are averages per-execution.
| This is done to make the numbers comparable with the way that the cost
| estimates are shown. Multiply by the loops value to get the total time
| actually spent in the node.
On Thu, Jul 07, 2022 at 01:45:19PM -0700, Peter Geoghegan wrote:
Plus you could probably
make some kind of concession in the direction of maintaining
compatibility with the current approach if you had to. Right?
The minimum would be to show the information in a way that makes it clear that
it's "new style" output showing a total and not an average, so that a person
who sees it knows how to interpret it (same for the web "explain tools")
A concession would be to show the current information *plus* total/raw values.
This thread is about how to display the existing values. But note that there's
a CF entry for also collecting more values to show things like min/max rows per
loop.
https://commitfest.postgresql.org/38/2765/
Add extra statistics to explain for Nested Loop
--
Justin
On Thu, Jul 7, 2022 at 2:41 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Thu, Jun 23, 2022 at 2:25 AM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Thu, Jun 23, 2022 at 1:04 AM David G. Johnston <
david.g.johnston@gmail.com> wrote:
- WRITE_FLOAT_FIELD(rows, "%.0f"); + WRITE_FLOAT_FIELD(rows, "%.2f");This one looks suspicious, though I haven't dug into the code to see
exactly what all is being touched. That it doesn't have an nloops
condition like everything else stands out.I was also thinking about that, but I don't see any harm when we
ultimately truncating that decimal
at a latter stage of code in case of loop = 1.
That change is in the path node which we anyway not going to target as
part of this change. We only want to change the display for actual
rows in Explain Analyze. So, I can't see how the quoted change can
help in any way.Agreed removed.
Few miscellaneous comments:
========================
*
static FullTransactionId XactTopFullTransactionId =
{InvalidTransactionId};
-static int nParallelCurrentXids = 0;
+static int nParallelCurrentXids = 0;Removed.
I don't see why this change is required.
* Can you please add a comment explaining why we are making this
change for actual rows?
Done
* Can you please write a test case unless there is some existing test
that covers the change by displaying actual rows values in decimal but
in that case patch should have that changed output test? If you don't
think we can reliably write such a test then please let me know the
reason?I think there are tests, and I have updated the results accordingly.
--
With Regards,
Amit Kapila.
--
Ibrar Ahmed
Attachments:
explain_float_row_v3.patchapplication/octet-stream; name=explain_float_row_v3.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index e29c2ae206..ae078d86de 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,8 @@
*/
#include "postgres.h"
+#include <math.h>
+
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/createas.h"
@@ -54,6 +56,9 @@ explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
#define X_CLOSE_IMMEDIATE 2
#define X_NOWHITESPACE 4
+/* Check if float/double has any decimal number */
+#define HAS_DECIMAL(x) (floor(x) != x)
+
static void ExplainOneQuery(Query *query, int cursorOptions,
IntoClause *into, ExplainState *es,
const char *queryString, ParamListInfo params,
@@ -1643,16 +1648,29 @@ ExplainNode(PlanState *planstate, List *ancestors,
double total_ms = 1000.0 * planstate->instrument->total / nloops;
double rows = planstate->instrument->ntuples / nloops;
+ /*
+ * If the number of loops is greater than one, display the
+ * actual rows up to two decimal places instead of rounding
+ * off the value.
+ */
if (es->format == EXPLAIN_FORMAT_TEXT)
{
if (es->timing)
+ {
+ appendStringInfo(es->str, " (actual time=%.3f..%.3f",
+ startup_ms, total_ms);
appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ (nloops == 1 || !HAS_DECIMAL(rows)) ?
+ " rows=%.0f loops=%.0f)" : " rows=%.2f loops=%.0f)",
+ rows, nloops);
+ }
else
+ {
appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
+ (nloops == 1 || !HAS_DECIMAL(rows)) ?
+ " (actual rows=%.0f loops=%.0f)" : " (actual rows=%.2f loops=%.0f)",
rows, nloops);
+ }
}
else
{
@@ -1663,7 +1681,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Rows", NULL, rows, (nloops == 1 || !HAS_DECIMAL(rows)) ? 0 : 2, es);
ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
}
}
@@ -1707,18 +1725,30 @@ ExplainNode(PlanState *planstate, List *ancestors,
rows = instrument->ntuples / nloops;
ExplainOpenWorker(n, es);
+ /*
+ * If the number of loops is greater than one, display the
+ * actual rows up to two decimal places instead of rounding
+ * off the value.
+ */
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
if (es->timing)
+ {
+ appendStringInfo(es->str, "actual time=%.3f..%.3f",
+ startup_ms, total_ms);
appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ (nloops == 1 || !HAS_DECIMAL(rows)) ?
+ " rows=%.0f loops=%.0f" : " rows=%.2f loops=%.0f",
+ rows, nloops);
+ }
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
+ {
+ appendStringInfo(es->str, (nloops == 1 || !HAS_DECIMAL(rows)) ?
+ "actual rows=%.0f loops=%.0f" : "actual rows=%.2f loops=%.0f",
rows, nloops);
+ }
}
else
{
@@ -1729,7 +1759,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Rows", NULL, rows,
+ (nloops == 1 || !HAS_DECIMAL(rows)) ? 0 : 2, es);
ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
}
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7555764c77..002e5cc33d 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2635,14 +2635,14 @@ order by tbl1.col1, tprt.col1;
insert into tbl1 values (1001), (1010), (1011);
explain (analyze, costs off, summary off, timing off)
select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=23 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=5 loops=5)
+ -> Append (actual rows=4.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
Index Cond: (col1 < tbl1.col1)
- -> Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
+ -> Index Scan using tprt2_idx on tprt_2 (actual rows=2.75 loops=4)
Index Cond: (col1 < tbl1.col1)
-> Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
Index Cond: (col1 < tbl1.col1)
@@ -2656,16 +2656,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
On Thu, Jul 7, 2022 at 3:14 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, Jun 23, 2022 at 2:25 AM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Thu, Jun 23, 2022 at 1:04 AM David G. Johnston <
david.g.johnston@gmail.com> wrote:
On Wed, Jun 22, 2022 at 12:11 PM Ibrar Ahmed <ibrar.ahmad@gmail.com>
wrote:
On Thu, Jun 23, 2022 at 12:01 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Jun 2, 2009, at 9:41 AM, Simon Riggs <simon@2ndQuadrant.com>
wrote:
You're right that the number of significant digits already exceeds
the
true accuracy of the computation. I think what Robert wants to see
is
the exact value used in the calc, so the estimates can be checked
more
thoroughly than is currently possible.
Bingo.
Uh, the planner's estimate *is* an integer. What was under discussion
(I thought) was showing some fractional digits in the case whereEXPLAIN
ANALYZE is outputting a measured row count that is an average over
multiple loops, and therefore isn't necessarily an integer. In that
case the measured value can be considered arbitrarily precise ---though
I think in practice one or two fractional digits would be plenty.
regards, tom lane
Hi,
I was looking at the TODO list and found that the issue requires
a quick fix. Attached is a patch which shows output like this.Quick code review:
+ "actual rows=%.0f loops=%.0f": " rows=%.2f loops=%.0f",
The leading space before the else block "rows" does not belong.
There should be a space after the colon.
Thanks, David for your quick response. I have updated the patch.
The word "actual" that you are dropping in the else block seems like it
should belong - it is a header for the entire section not just a modifier
for the word "rows". This is evidenced by the timing block verbiage where
rows is standalone and the word actual comes before time. In short, only
the format specifier should change under the current scheme. Both sections.- WRITE_FLOAT_FIELD(rows, "%.0f"); + WRITE_FLOAT_FIELD(rows, "%.2f");This one looks suspicious, though I haven't dug into the code to see
exactly what all is being touched. That it doesn't have an nloops
condition like everything else stands out.I was also thinking about that, but I don't see any harm when we
ultimately truncating that decimal
at a latter stage of code in case of loop = 1.
Thanks for the patch.
Thanks for the review.
1) There are some existing regression tests that are failing, you should update the expect files accordingly for the same: --- /home/vignesh/postgres/src/test/regress/expected/select_parallel.out 2022-05-18 20:51:46.874818044 +0530 +++ /home/vignesh/postgres/src/test/regress/results/select_parallel.out 2022-07-07 15:27:34.450440922 +0530 @@ -545,17 +545,17 @@ explain (analyze, timing off, summary off, costs off) select count(*) from tenk1, tenk2 where tenk1.hundred > 1 and tenk2.thousand=0; - QUERY PLAN --------------------------------------------------------------------------- + QUERY PLAN+----------------------------------------------------------------------------- Aggregate (actual rows=1 loops=1) -> Nested Loop (actual rows=98000 loops=1) -> Seq Scan on tenk2 (actual rows=10 loops=1) Filter: (thousand = 0) Rows Removed by Filter: 9990 - -> Gather (actual rows=9800 loops=10) + -> Gather (actual rows=9800.00 loops=10) Workers Planned: 4 Workers Launched: 4 - -> Parallel Seq Scan on tenk1 (actual rows=1960 loops=50) + -> Parallel Seq Scan on tenk1 (actual rows=1960.00 loops=50) Filter: (hundred > 1)test select_parallel ... FAILED 744 ms
partition_prune ... FAILED 861 ms
explain ... FAILED 134 ms
memoize ... FAILED 250 ms2) This change is not required as part of this patch: --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -122,7 +122,7 @@ bool bsysscan = false; * lookups as fast as possible. */ static FullTransactionId XactTopFullTransactionId = {InvalidTransactionId}; -static int nParallelCurrentXids = 0; +static int nParallelCurrentXids = 0; static TransactionId *ParallelCurrentXids;
I have fixed the regression and removed non-related code.
Regards,
Vignesh
--
Ibrar Ahmed
On Thu, Jul 7, 2022 at 10:53 PM Greg Stark <stark@mit.edu> wrote:
- -> Parallel Seq Scan on tenk1 (actual rows=1960
loops=50)
+ -> Parallel Seq Scan on tenk1 (actual rows=1960.00
At the not inconsiderable risk of bike-shedding....
I'm wondering if printing something like 0.00 will be somewhat
deceptive when the real value is non-zero but less than 1 row per 200
loops. I wonder if the number of decimal places should be calculated
to produce a minimum of one non-zero digit for non-zero values.--
greg
+ -> Parallel Seq Scan on tenk1 (actual rows=1960.00
I have added a new check to remove any ".00" from the output because in
the case of parallel queries we are getting that. Secondly, it is
disturbing many test case outputs.
--
Ibrar Ahmed
On Fri, Jul 8, 2022 at 3:50 AM Justin Pryzby <pryzby@telsasoft.com> wrote:
On Thu, Jul 07, 2022 at 04:21:37PM -0400, Robert Haas wrote:
I mean, what I really want here if I'm honest is to not have the
system divide the number of rows by the loop count. And it sort of
sounds like maybe that's what you want, too. You want to know whether
the loop count is actually zero, not whether it's close to zero when
you divide it by some number that might be gigantic....
involves a dozen or two different nested loops, and if we didn't
insist on dividing the time by the loop count, it would be MUCH EASIER
to figure out whether the time spent in the Index Scan is a
significant percentage of the total time or not.I think the guiding princible for what to do should be to reduce how much is
needed to explain about how to interpret what explain is showing...The docs say this:
| In such cases, the loops value reports the total number of executions of the
| node, and the actual time and rows values shown are averages per-execution.
| This is done to make the numbers comparable with the way that the cost
| estimates are shown. Multiply by the loops value to get the total time
| actually spent in the node.On Thu, Jul 07, 2022 at 01:45:19PM -0700, Peter Geoghegan wrote:
Plus you could probably
make some kind of concession in the direction of maintaining
compatibility with the current approach if you had to. Right?The minimum would be to show the information in a way that makes it clear that
it's "new style" output showing a total and not an average, so that a person
who sees it knows how to interpret it (same for the web "explain tools")A concession would be to show the current information *plus* total/raw values.
This thread is about how to display the existing values.
I feel the discussion has slightly deviated which makes it unclear
whether this patch is required or not?
--
With Regards,
Amit Kapila.
On Fri, Jul 22, 2022 at 6:47 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
I feel the discussion has slightly deviated which makes it unclear
whether this patch is required or not?
My opinion is that showing some fractional digits at least when
loops>1 would be better than what we have now. It might not be the
best thing we could do, but it would be better than doing nothing.
--
Robert Haas
EDB: http://www.enterprisedb.com
The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, passed
Spec compliant: not tested
Documentation: not tested
Verified patch 'explain_float_row_v3.patch' on master & REL_15_STABLE branches.
The new status of this patch is: Ready for Committer
The following review has been posted through the commitfest application:
make installcheck-world: tested, failed
Implements feature: tested, failed
Spec compliant: not tested
Documentation: not tested
LGTM
The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, passed
Spec compliant: not tested
Documentation: not tested
The previous review was incorrectly posted. Updating the pat.ch review
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Jul 22, 2022 at 6:47 AM Amit Kapila <amit.kapila16@gmail.com> wr=
ote:
I feel the discussion has slightly deviated which makes it unclear
whether this patch is required or not?
My opinion is that showing some fractional digits at least when
loops>1 would be better than what we have now. It might not be the
best thing we could do, but it would be better than doing nothing.
Yeah, I think that is a reasonable compromise.
I took a brief look through the patch, and I have some review
comments:
* Code like this is pretty awful:
appendStringInfo(es->str,
(nloops =3D=3D 1 || !HAS_DECIMAL(rows)) ?
" rows=3D%.0f loops=3D%.0f)" : " rows=3D%=
.2f loops=3D%.0f)",
rows, nloops);
Don't use variable format strings. They're hard to read and they
probably defeat compile-time checks that the arguments match the
format string. You could use a "*" field width instead, ie
appendStringInfo(es->str,
" rows=3D%.*f loops=3D%.0f)",
(nloops =3D=3D 1 || !HAS_DECIMAL(rows)) ?=
2 : 0,
rows, nloops);
That'd also allow you to reduce the code churn you've added by
splitting some appendStringInfo calls.
* I'm fairly concerned about how stable this'll be in the buildfarm,
in particular I fear HAS_DECIMAL() is not likely to give consistent
results across platforms. I gather that an earlier version of the patch
tried to check whether the fractional part would be zero to two decimal
places, rather than whether it's exactly zero. Probably want to put
back something like that.
* Another thought is that the non-text formats tend to prize output
consistency over readability, so maybe we should just always use 2
fractional digits there, rather than trying to minimize visible changes.
* We need some doc adjustments, surely, to explain what the heck this
means.
regards, tom lane
On 22/7/2022 16:47, Amit Kapila wrote:
I feel the discussion has slightly deviated which makes it unclear
whether this patch is required or not?
After quick review I want to express my thoughts.
At first, We have been waiting for this feature for years. Often clients
give an explain to us where we see something like:
"rows=0, loops=1000000".
Without verbose mode, I can't even understand whether this node produces
any rows or not.
So, I think this feature is useful for parameterized plans mostly.
Also, printing two decimal digits or even three isn't meaningful -
sometimes we have a plan where number of loops is about 1E6 or even 1E7,
but number of real rows is equal 100 or 1000.
To overcome this issue, I see two options:
1. Show the exact number of tuples without division by loops (fair case
but invasive and bad for automation tools).
2. Show rows in scientific format like X.XXEXX.
I vote for second option.
--
regards,
Andrey Lepikhov
Postgres Professional
On Sun, Nov 6, 2022 at 10:12 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Jul 22, 2022 at 6:47 AM Amit Kapila <amit.kapila16@gmail.com>
wr=
ote:I feel the discussion has slightly deviated which makes it unclear
whether this patch is required or not?My opinion is that showing some fractional digits at least when
loops>1 would be better than what we have now. It might not be the
best thing we could do, but it would be better than doing nothing.Yeah, I think that is a reasonable compromise.
Thanks, I have modified everything as suggested, except one point
I took a brief look through the patch, and I have some review
comments:* Code like this is pretty awful:
appendStringInfo(es->str,
(nloops =3D=3D 1 || !HAS_DECIMAL(rows)) ?
" rows=3D%.0f loops=3D%.0f)" : " rows=3D%=
.2f loops=3D%.0f)",
rows, nloops);Don't use variable format strings. They're hard to read and they
probably defeat compile-time checks that the arguments match the
format string. You could use a "*" field width instead, ieappendStringInfo(es->str,
" rows=3D%.*f loops=3D%.0f)",
(nloops =3D=3D 1 || !HAS_DECIMAL(rows)) ?=
2 : 0,
rows, nloops);That'd also allow you to reduce the code churn you've added by
splitting some appendStringInfo calls.* I'm fairly concerned about how stable this'll be in the buildfarm,
in particular I fear HAS_DECIMAL() is not likely to give consistent
results across platforms. I gather that an earlier version of the patch
tried to check whether the fractional part would be zero to two decimal
places, rather than whether it's exactly zero. Probably want to put
back something like that.* Another thought is that the non-text formats tend to prize output
consistency over readability, so maybe we should just always use 2
fractional digits there, rather than trying to minimize visible changes.In that, we need to adjust a lot of test case outputs.
* We need some doc adjustments, surely, to explain what the heck this
means.
regards, tom lane
--
Ibrar Ahmed
On Wed, 4 Jan 2023 at 10:05, Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
Thanks, I have modified everything as suggested, except one point
Don't use variable format strings. They're hard to read and they
probably defeat compile-time checks that the arguments match the
format string. You could use a "*" field width instead, ie
...
* Another thought is that the non-text formats tend to prize output
consistency over readability, so maybe we should just always use 2
fractional digits there, rather than trying to minimize visible changes.In that, we need to adjust a lot of test case outputs.
* We need some doc adjustments, surely, to explain what the heck this
means.
That sounds like three points :) But they seem like pretty good
arguments to me and straightforward if a little tedious to adjust.
This patch was marked Returned with Feedback and then later Waiting on
Author. And it hasn't had any updates since January. What is the state
on this feedback? If it's already done we can set the patch to Ready
for Commit and if not do you disagree with the proposed changes?
It's actually the kind of code cleanup changes I'm reluctant to bump a
patch for. It's not like a committer can't make these kinds of changes
when committing. But there are so many patches they're likely to just
focus on a different patch when there are adjustments like this
pending.
--
Gregory Stark
As Commitfest Manager
On Mon, Mar 20, 2023 at 7:56 PM Gregory Stark (as CFM) <stark.cfm@gmail.com>
wrote:
On Wed, 4 Jan 2023 at 10:05, Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
Thanks, I have modified everything as suggested, except one point
Don't use variable format strings. They're hard to read and they
probably defeat compile-time checks that the arguments match the
format string. You could use a "*" field width instead, ie...
* Another thought is that the non-text formats tend to prize output
consistency over readability, so maybe we should just always use 2
fractional digits there, rather than trying to minimize visiblechanges.
In that, we need to adjust a lot of test case outputs.
* We need some doc adjustments, surely, to explain what the heck this
means.That sounds like three points :) But they seem like pretty good
arguments to me and straightforward if a little tedious to adjust.This patch was marked Returned with Feedback and then later Waiting on
Author. And it hasn't had any updates since January. What is the state
on this feedback? If it's already done we can set the patch to Ready
for Commit and if not do you disagree with the proposed changes?If there is a consensus to modify the test cases' output, I am willing to
make the necessary changes and adjust the patch accordingly. However,
if there is a preference to keep the output of certain test cases unchanged,
I can rebase and modify the patch accordingly to accommodate those
preferences.
It's actually the kind of code cleanup changes I'm reluctant to bump a
patch for. It's not like a committer can't make these kinds of changes
when committing. But there are so many patches they're likely to just
focus on a different patch when there are adjustments like this
pending.--
Gregory Stark
As Commitfest Manager
--
Ibrar Ahmed
On 8 Jun 2023, at 19:49, Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Mon, Mar 20, 2023 at 7:56 PM Gregory Stark (as CFM) <stark.cfm@gmail.com <mailto:stark.cfm@gmail.com>> wrote:
This patch was marked Returned with Feedback and then later Waiting on
Author. And it hasn't had any updates since January. What is the state
on this feedback? If it's already done we can set the patch to Ready
for Commit and if not do you disagree with the proposed changes?If there is a consensus to modify the test cases' output, I am willing to
make the necessary changes and adjust the patch accordingly. However,
if there is a preference to keep the output of certain test cases unchanged,
I can rebase and modify the patch accordingly to accommodate those preferences.
As there hasn't been any other comments I suggest updating your patch to
address Tom's comments to see if we can make progress here.
--
Daniel Gustafsson
On 3 Jul 2023, at 18:34, Daniel Gustafsson <daniel@yesql.se> wrote:
On 8 Jun 2023, at 19:49, Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Mon, Mar 20, 2023 at 7:56 PM Gregory Stark (as CFM) <stark.cfm@gmail.com <mailto:stark.cfm@gmail.com>> wrote:This patch was marked Returned with Feedback and then later Waiting on
Author. And it hasn't had any updates since January. What is the state
on this feedback? If it's already done we can set the patch to Ready
for Commit and if not do you disagree with the proposed changes?If there is a consensus to modify the test cases' output, I am willing to
make the necessary changes and adjust the patch accordingly. However,
if there is a preference to keep the output of certain test cases unchanged,
I can rebase and modify the patch accordingly to accommodate those preferences.As there hasn't been any other comments I suggest updating your patch to
address Tom's comments to see if we can make progress here.
Since there hasn't been any updates here, and the thread has been stalled, I'm
marking this returned with feedback. Please feel free to resubmit a version of
the patch addressing comments to a future CF.
--
Daniel Gustafsson
On 01.08.2023 23:29, Daniel Gustafsson wrote:
On 3 Jul 2023, at 18:34, Daniel Gustafsson <daniel@yesql.se> wrote:
On 8 Jun 2023, at 19:49, Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Mon, Mar 20, 2023 at 7:56 PM Gregory Stark (as CFM) <stark.cfm@gmail.com <mailto:stark.cfm@gmail.com>> wrote:
This patch was marked Returned with Feedback and then later Waiting on
Author. And it hasn't had any updates since January. What is the state
on this feedback? If it's already done we can set the patch to Ready
for Commit and if not do you disagree with the proposed changes?If there is a consensus to modify the test cases' output, I am willing to
make the necessary changes and adjust the patch accordingly. However,
if there is a preference to keep the output of certain test cases unchanged,
I can rebase and modify the patch accordingly to accommodate those preferences.As there hasn't been any other comments I suggest updating your patch to
address Tom's comments to see if we can make progress here.Since there hasn't been any updates here, and the thread has been stalled, I'm
marking this returned with feedback. Please feel free to resubmit a version of
the patch addressing comments to a future CF.--
Daniel Gustafsson
Hi everybody,
When the total number of returned tuples is less than the number of
loops currently shows 'rows = 0'. This can mislead users into thinking
that no rows were returned at all, even though some might have appeared
occasionally. For example, if there is 1 tuple over 100 loops, the
average is 0.01 rows per loop, but as displayed it simply looks like zero.
To clarify this situation, it has been suggested that display rows with
two decimal places in scenarios where 'loops > 1 && ntuples < loops'. In
other words, show something like 'rows = 0.01' instead of 'rows=0'. This
minor change would make it evident that rows did occur, just very
infrequently.
For all other cases, the current formatting would remain the same. This
approach aims to provide a more nuanced understanding of query execution
behavior without introducing unnecessary complexity or false precision.
I would appreciate any thoughts or feedback on this proposal.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
explain_float_row_v4.patchtext/x-patch; charset=UTF-8; name=explain_float_row_v4.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index a201ed3082..b3f7fa8743 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1981,14 +1981,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
+ appendStringInfo(es->str, " (actual");
if (es->timing)
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, " time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
else
- appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ appendStringInfo(es->str," rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -1999,8 +1999,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 2, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
}
else if (es->analyze)
@@ -2052,14 +2060,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
+ appendStringInfo(es->str, "actual");
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, " time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ appendStringInfo(es->str," rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -2070,8 +2078,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 2, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index c52bc40e81..cf50776ef7 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -3041,16 +3041,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5.00)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3.00)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
Hello,
Le jeu. 12 déc. 2024 à 12:57, Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
a écrit :
On 01.08.2023 23:29, Daniel Gustafsson wrote:
On 3 Jul 2023, at 18:34, Daniel Gustafsson <daniel@yesql.se> wrote:
On 8 Jun 2023, at 19:49, Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Mon, Mar 20, 2023 at 7:56 PM Gregory Stark (as CFM) <stark.cfm@gmail.com <mailto:stark.cfm@gmail.com>> wrote:
This patch was marked Returned with Feedback and then later Waiting on
Author. And it hasn't had any updates since January. What is the state
on this feedback? If it's already done we can set the patch to Ready
for Commit and if not do you disagree with the proposed changes?If there is a consensus to modify the test cases' output, I am willing
to
make the necessary changes and adjust the patch accordingly. However,
if there is a preference to keep the output of certain test casesunchanged,
I can rebase and modify the patch accordingly to accommodate those
preferences.
As there hasn't been any other comments I suggest updating your patch to
address Tom's comments to see if we can make progress here.Since there hasn't been any updates here, and the thread has been
stalled, I'm
marking this returned with feedback. Please feel free to resubmit a
version of
the patch addressing comments to a future CF.
--
Daniel GustafssonHi everybody,
When the total number of returned tuples is less than the number of
loops currently shows 'rows = 0'. This can mislead users into thinking
that no rows were returned at all, even though some might have appeared
occasionally. For example, if there is 1 tuple over 100 loops, the
average is 0.01 rows per loop, but as displayed it simply looks like zero.To clarify this situation, it has been suggested that display rows with
two decimal places in scenarios where 'loops > 1 && ntuples < loops'. In
other words, show something like 'rows = 0.01' instead of 'rows=0'. This
minor change would make it evident that rows did occur, just very
infrequently.For all other cases, the current formatting would remain the same. This
approach aims to provide a more nuanced understanding of query execution
behavior without introducing unnecessary complexity or false precision.I would appreciate any thoughts or feedback on this proposal.
Thanks for your patch, this looks like a very interesting feature that I'd
like to see in a future release.
It did a quick run: patch OK, make OK, make install OK, but make check
fails quite a lot on partition_prune.sql.
I guess it would need some work on partition_prune.sql tests and perhaps
also on the docs.
Thanks again.
--
Guillaume.
On 11.01.2025 12:15, Guillaume Lelarge wrote:
Thanks for your patch, this looks like a very interesting feature that
I'd like to see in a future release.It did a quick run: patch OK, make OK, make install OK, but make check
fails quite a lot on partition_prune.sql.I guess it would need some work on partition_prune.sql tests and
perhaps also on the docs.Thanks again.
--
Guillaume.
Yes, certainly. I have fixed partition_prune.sql. In the documentation
example for EXPLAIN ANALYZE where loops is greater than one, I updated
how 'rows' and 'loops' values are displayed so they appear as decimal
fractions with two digits after the decimal point.
I attached fixed patch.
Any suggestions?
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v5-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patchtext/x-patch; charset=UTF-8; name=v5-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patchDownload
From a934fbd278350edf7a3a3e7fe620a0027ceb2580 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Date: Sat, 11 Jan 2025 13:59:34 +0300
Subject: [PATCH] Clarify display of rows and loops as decimal fractions
When the average number of rows is small compared to the number of loops,
both rows and loops are displayed as decimal fractions with two digits
after the decimal point in EXPLAIN ANALYZE.
---
doc/src/sgml/perform.sgml | 2 +-
src/backend/commands/explain.c | 49 +++++++++++++------
src/test/regress/expected/partition_prune.out | 10 ++--
src/test/regress/sql/partition_prune.sql | 2 +-
4 files changed, 40 insertions(+), 23 deletions(-)
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aaba..da7a19f4b8 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,7 +730,7 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
Index Cond: (unique1 < 10)
Buffers: shared hit=2
- -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
+ -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1.00 loops=10.00)
Index Cond: (unique2 = t1.unique2)
Buffers: shared hit=24 read=6
Planning:
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82e..200294b756 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1981,14 +1981,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
+ appendStringInfo(es->str, " (actual");
if (es->timing)
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, " time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
else
- appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ appendStringInfo(es->str," rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -1999,8 +1999,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 2, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
}
else if (es->analyze)
@@ -2052,14 +2060,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
+ appendStringInfo(es->str, "actual");
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, " time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ appendStringInfo(es->str," rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -2070,8 +2078,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 2, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index c52bc40e81..dc6a3cb54a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2338,7 +2338,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+(?:\.\d+)?', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
@@ -3041,16 +3041,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5.00)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3.00)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index d67598d5c7..bb1e09a536 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -571,7 +571,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+(?:\.\d+)?', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
--
2.34.1
Le sam. 11 janv. 2025 à 12:10, Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
a écrit :
On 11.01.2025 12:15, Guillaume Lelarge wrote:
Thanks for your patch, this looks like a very interesting feature that I'd
like to see in a future release.It did a quick run: patch OK, make OK, make install OK, but make check
fails quite a lot on partition_prune.sql.I guess it would need some work on partition_prune.sql tests and perhaps
also on the docs.Thanks again.
--
Guillaume.Yes, certainly. I have fixed partition_prune.sql. In the documentation
example for EXPLAIN ANALYZE where loops is greater than one, I updated how
'rows' and 'loops' values are displayed so they appear as decimal fractions
with two digits after the decimal point.I attached fixed patch.
Any suggestions?
Thanks for the updated patch.
"make check" is happy, so no more suggestions, except you should register
it in the next commitfest.
--
Guillaume.
On 11.01.2025 17:02, Guillaume Lelarge wrote:
Le sam. 11 janv. 2025 à 12:10, Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> a écrit :On 11.01.2025 12:15, Guillaume Lelarge wrote:
Thanks for your patch, this looks like a very interesting feature
that I'd like to see in a future release.It did a quick run: patch OK, make OK, make install OK, but make
check fails quite a lot on partition_prune.sql.I guess it would need some work on partition_prune.sql tests and
perhaps also on the docs.Thanks again.
--
Guillaume.Yes, certainly. I have fixed partition_prune.sql. In the
documentation example for EXPLAIN ANALYZE where loops is greater
than one, I updated how 'rows' and 'loops' values are displayed so
they appear as decimal fractions with two digits after the decimal
point.I attached fixed patch.
Any suggestions?
Thanks for the updated patch.
"make check" is happy, so no more suggestions, except you should
register it in the next commitfest.--
Guillaume.
Done!
https://commitfest.postgresql.org/52/5501/
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
On 11.01.2025 14:10, Ilia Evdokimov wrote:
On 11.01.2025 12:15, Guillaume Lelarge wrote:
Thanks for your patch, this looks like a very interesting feature
that I'd like to see in a future release.It did a quick run: patch OK, make OK, make install OK, but make
check fails quite a lot on partition_prune.sql.I guess it would need some work on partition_prune.sql tests and
perhaps also on the docs.Thanks again.
--
Guillaume.Yes, certainly. I have fixed partition_prune.sql. In the documentation
example for EXPLAIN ANALYZE where loops is greater than one, I updated
how 'rows' and 'loops' values are displayed so they appear as decimal
fractions with two digits after the decimal point.I attached fixed patch.
Any suggestions?
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
I guess, it's not ideal to modify the existing example in the
documentation of the v5 patch because readers wouldn't immediately
understand why decimal fractions appear there. Instead, I'll add a brief
note in the documentation clarifying how rows and loops are displayed
when the average row count is below one.
The changes the of documentation are attached v6 patch.
If you have any other suggestions or different opinions, I'd be happy to
discuss them.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v6-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patchtext/x-patch; charset=UTF-8; name=v6-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patchDownload
From 08f92c7e11829045014598e1dcc042f3e5a1e1a3 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Date: Mon, 13 Jan 2025 23:01:44 +0300
Subject: [PATCH] Clarify display of rows and loops as decimal fractions
When the average number of rows is small compared to the number of loops,
both rows and loops are displayed as decimal fractions with two digits
after the decimal point in EXPLAIN ANALYZE.
---
doc/src/sgml/perform.sgml | 6 ++-
src/backend/commands/explain.c | 49 +++++++++++++------
src/test/regress/expected/partition_prune.out | 10 ++--
src/test/regress/sql/partition_prune.sql | 2 +-
4 files changed, 44 insertions(+), 23 deletions(-)
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aaba..3f13d17fe9 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -757,7 +757,11 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
comparable with the way that the cost estimates are shown. Multiply by
the <literal>loops</literal> value to get the total time actually spent in
the node. In the above example, we spent a total of 0.030 milliseconds
- executing the index scans on <literal>tenk2</literal>.
+ executing the index scans on <literal>tenk2</literal>. If a subplan node
+ is executed multiple times and the average number of rows is less than one,
+ the rows and <literal>loops</literal> values are shown as a decimal fraction
+ (with two digits after the decimal point) to indicate that some rows
+ were actually processed rather than simply rounding down to zero.
</para>
<para>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82e..200294b756 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1981,14 +1981,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
+ appendStringInfo(es->str, " (actual");
if (es->timing)
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, " time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
else
- appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ appendStringInfo(es->str," rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -1999,8 +1999,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 2, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
}
else if (es->analyze)
@@ -2052,14 +2060,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
+ appendStringInfo(es->str, "actual");
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, " time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ appendStringInfo(es->str," rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -2070,8 +2078,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 2, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index c52bc40e81..dc6a3cb54a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2338,7 +2338,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+(?:\.\d+)?', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
@@ -3041,16 +3041,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5.00)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3.00)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index d67598d5c7..bb1e09a536 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -571,7 +571,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+(?:\.\d+)?', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
--
2.34.1
Hi
Em seg., 13 de jan. de 2025 às 17:18, Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> escreveu:
I guess, it's not ideal to modify the existing example in the documentation of the v5 patch because readers wouldn't immediately understand why decimal fractions appear there. Instead, I'll add a brief note in the documentation clarifying how rows and loops are displayed when the average row count is below one.
The changes the of documentation are attached v6 patch.
v6 is not applying on master, could you please rebase?
git apply v6-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patch
error: patch failed: src/test/regress/expected/partition_prune.out:3041
error: src/test/regress/expected/partition_prune.out: patch does not apply
--
Matheus Alcantara
On 07.02.2025 22:59, Matheus Alcantara wrote:
Hi
v6 is not applying on master, could you please rebase?git apply v6-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patch
error: patch failed: src/test/regress/expected/partition_prune.out:3041
error: src/test/regress/expected/partition_prune.out: patch does not apply
Hi
Strange, I don't have any problems to apply it on master.
postgres$ git branch
* master
postgres$ git pull
Already up to date.
postgres$ git apply
v6-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patch
postgres$
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Em sex., 7 de fev. de 2025 às 17:41, Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> escreveu:
Strange, I don't have any problems to apply it on master.
postgres$ git branch
* master
postgres$ git pull
Already up to date.
postgres$ git apply
v6-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patch
postgres$
Just for reference I'm trying to apply based on commit fb056564ec5.
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=fb056564ec5bc1c18dd670c963c893cdb6de927e
--
Matheus Alcantara
On 08.02.2025 00:01, Matheus Alcantara wrote:
Em sex., 7 de fev. de 2025 às 17:41, Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> escreveu:Strange, I don't have any problems to apply it on master.
postgres$ git branch
* master
postgres$ git pull
Already up to date.
postgres$ git apply
v6-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patch
postgres$Just for reference I'm trying to apply based on commit fb056564ec5.
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=fb056564ec5bc1c18dd670c963c893cdb6de927e
You are right, because two commits were appeared after creating v6-patch
on partition_prune.out and patch v6 must not have applied on master.
Then I created v7 patch rebased on fb056564ec5 . Thank for your remark!
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v7-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patchtext/x-patch; charset=UTF-8; name=v7-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patchDownload
From d94d30618b6f363a54afe5457ef8fe89476e096e Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Date: Sat, 8 Feb 2025 00:15:54 +0300
Subject: [PATCH] Clarify display of rows and loops as decimal fractions
When the average number of rows is small compared to the number of loops,
both rows and loops are displayed as decimal fractions with two digits
after the decimal point in EXPLAIN ANALYZE.
---
doc/src/sgml/perform.sgml | 6 ++-
src/backend/commands/explain.c | 49 +++++++++++++------
src/test/regress/expected/partition_prune.out | 10 ++--
src/test/regress/sql/partition_prune.sql | 2 +-
4 files changed, 44 insertions(+), 23 deletions(-)
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aaba..3f13d17fe9 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -757,7 +757,11 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
comparable with the way that the cost estimates are shown. Multiply by
the <literal>loops</literal> value to get the total time actually spent in
the node. In the above example, we spent a total of 0.030 milliseconds
- executing the index scans on <literal>tenk2</literal>.
+ executing the index scans on <literal>tenk2</literal>. If a subplan node
+ is executed multiple times and the average number of rows is less than one,
+ the rows and <literal>loops</literal> values are shown as a decimal fraction
+ (with two digits after the decimal point) to indicate that some rows
+ were actually processed rather than simply rounding down to zero.
</para>
<para>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82e..200294b756 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1981,14 +1981,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
+ appendStringInfo(es->str, " (actual");
if (es->timing)
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, " time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
else
- appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ appendStringInfo(es->str," rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -1999,8 +1999,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 2, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
}
else if (es->analyze)
@@ -2052,14 +2060,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
+ appendStringInfo(es->str, "actual");
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, " time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ appendStringInfo(es->str," rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -2070,8 +2078,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+ if (nloops > 1 && planstate->instrument->ntuples < nloops)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 2, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index e667503c96..b45bb83c0f 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2367,7 +2367,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+(?:\.\d+)?', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
@@ -3070,16 +3070,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5.00)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3.00)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 730545e86a..ea38ae6990 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -586,7 +586,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+(?:\.\d+)?', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
--
2.34.1
Thanks for the new patch version.
-- v7-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patch
+ if (nloops > 1 && planstate->instrument->ntuples < nloops) + appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);
Sorry, but why do the 'loops' need to be changed? IIUC the issue is
only with the
rows field? When testing this patch I got some results similar with this:
Index Scan using t_a_idx on t (cost=0.14..0.28 rows=1 width=8)
(actual time=0.049..0.049 rows=0.50 loops=2.00)
When the total number of returned tuples is less than the number of
loops currently shows 'rows = 0'. This can mislead users into thinking
that no rows were returned at all, even though some might have appeared
occasionally.
I think that this can happen when the returned rows and the loops are small
enough to result in a 'row' value like 0.00045? I'm not sure if we have
"bigger" values (e.g 1074(ntuples) / 117(nloops) which would result in 9.17
rows) this would also be true, what do you think? If you could provide
an example of this case would be great!
- executing the index scans on <literal>tenk2</literal>.
+ executing the index scans on <literal>tenk2</literal>. If a subplan node
+ is executed multiple times and the average number of rows is less than one,
+ the rows and <literal>loops</literal> values are shown as a
decimal fraction
+ (with two digits after the decimal point) to indicate that some rows
+ were actually processed rather than simply rounding down to zero.
* I think that it would be good to mention what a 'row' value in
decimal means. For
example, if its says "0.1 rows" the user should assume that typically 0 rows
will be returned but sometimes it can return 1 or more.
* There are more spaces than necessary before "If a subplan node ..."
* Maybe wrap 'rows' with <literal> </literal>?
--
Matheus Alcantara
On 10.02.2025 18:32, Matheus Alcantara wrote:
Thanks for the new patch version.
-- v7-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patch
+ if (nloops > 1 && planstate->instrument->ntuples < nloops) + appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);Sorry, but why do the 'loops' need to be changed? IIUC the issue is
only with the
rows field? When testing this patch I got some results similar with this:
Index Scan using t_a_idx on t (cost=0.14..0.28 rows=1 width=8)
(actual time=0.049..0.049 rows=0.50 loops=2.00)
The only reason I added this was to make it clear to the user that loops
1, along with the 'rows' value. If no one finds it useful and it only
raises more questions, I can remove it.
When the total number of returned tuples is less than the number of
loops currently shows 'rows = 0'. This can mislead users into thinking
that no rows were returned at all, even though some might have appeared
occasionally.I think that this can happen when the returned rows and the loops are small
enough to result in a 'row' value like 0.00045? I'm not sure if we have
"bigger" values (e.g 1074(ntuples) / 117(nloops) which would result in 9.17
rows) this would also be true, what do you think? If you could provide
an example of this case would be great!
Based on what was discussed earlier in the thread, there are cases with
large loops [0]/messages/by-id/a9393107-6bb9-c835-50b7-c0f453a514b8@postgrespro.ru. However, I believe it's better not to display average
rows with excessively long digits or in scientific notation. And, of
course, I agree with you regarding small values. I think we should also
add a check to ensure that the total rows is actually greater than zero.
When the total rows is zero, we could simply display it as an integer
without decimals. It could help users average rows is very small but not
zero. What do you think about this approach?
- executing the index scans on <literal>tenk2</literal>. + executing the index scans on <literal>tenk2</literal>. If a subplan node + is executed multiple times and the average number of rows is less than one, + the rows and <literal>loops</literal> values are shown as a decimal fraction + (with two digits after the decimal point) to indicate that some rows + were actually processed rather than simply rounding down to zero.* I think that it would be good to mention what a 'row' value in
decimal means. For
example, if its says "0.1 rows" the user should assume that typically 0 rows
will be returned but sometimes it can return 1 or more.* There are more spaces than necessary before "If a subplan node ..."
* Maybe wrap 'rows' with <literal> </literal>?
I agree with the last two points. As for the first one—maybe we could
simply state that the average rows value can be decimal, especially for
very small values?
[0]: /messages/by-id/a9393107-6bb9-c835-50b7-c0f453a514b8@postgrespro.ru
/messages/by-id/a9393107-6bb9-c835-50b7-c0f453a514b8@postgrespro.ru
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Em seg., 10 de fev. de 2025 às 13:38, Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> escreveu:
-- v7-0001-Clarify-display-of-rows-and-loops-as-decimal-fraction.patch
+ if (nloops > 1 && planstate->instrument->ntuples < nloops) + appendStringInfo(es->str," rows=%.2f loops=%.2f)", rows, nloops);Sorry, but why do the 'loops' need to be changed? IIUC the issue is
only with the
rows field? When testing this patch I got some results similar with this:
Index Scan using t_a_idx on t (cost=0.14..0.28 rows=1 width=8)
(actual time=0.049..0.049 rows=0.50 loops=2.00)The only reason I added this was to make it clear to the user that loops
1, along with the 'rows' value. If no one finds it useful and it only
raises more questions, I can remove it.
Ok, I get it. IMHO the current behaviour of using %.0f for 'loops' would be
better, but let's see if anyone else has opinions about it.
When the total number of returned tuples is less than the number of
loops currently shows 'rows = 0'. This can mislead users into thinking
that no rows were returned at all, even though some might have appeared
occasionally.I think that this can happen when the returned rows and the loops are small
enough to result in a 'row' value like 0.00045? I'm not sure if we have
"bigger" values (e.g 1074(ntuples) / 117(nloops) which would result in 9.17
rows) this would also be true, what do you think? If you could provide
an example of this case would be great!Based on what was discussed earlier in the thread, there are cases with
large loops [0]. However, I believe it's better not to display average
rows with excessively long digits or in scientific notation. And, of
course, I agree with you regarding small values. I think we should also
add a check to ensure that the total rows is actually greater than zero.
When the total rows is zero, we could simply display it as an integer
without decimals. It could help users average rows is very small but not
zero. What do you think about this approach?
Yeah, I agree with you about the long digits. My question is more about why do
we need the planstate->instrument->ntuples < nloops check? I tried to remove
this check and I got a lot of EXPLAIN output that shows 'rows' values with .00,
so I'm just trying to understand the reason. From what I've understood about
this thread is that just avoiding .00 decimals of 'rows' values that could be
just integers would be enough, is that right or I'm missing something here? I'm
just worried if we could have a scenario where nloops > 1 &&
planstate->instrument->ntuples < nloops which would make the 'rows' not be
formatted correctly.
- executing the index scans on <literal>tenk2</literal>. + executing the index scans on <literal>tenk2</literal>. If a subplan node + is executed multiple times and the average number of rows is less than one, + the rows and <literal>loops</literal> values are shown as a decimal fraction + (with two digits after the decimal point) to indicate that some rows + were actually processed rather than simply rounding down to zero.* I think that it would be good to mention what a 'row' value in
decimal means. For
example, if its says "0.1 rows" the user should assume that typically 0 rows
will be returned but sometimes it can return 1 or more.* There are more spaces than necessary before "If a subplan node ..."
* Maybe wrap 'rows' with <literal> </literal>?
I agree with the last two points. As for the first one—maybe we could
simply state that the average rows value can be decimal, especially for
very small values?
I'm just not sure about the "small values"; the 'rows' in decimal will only
happen with small values? What would be a "small value" in this context? My main
point here is more that I think that it would be good to mention *why* the
'rows' can be decimal, not just describe that it could be decimal.
--
Matheus Alcantara
On 10.02.2025 23:43, Matheus Alcantara wrote:
When the total number of returned tuples is less than the number of
loops currently shows 'rows = 0'. This can mislead users into thinking
that no rows were returned at all, even though some might have appeared
occasionally.I think that this can happen when the returned rows and the loops are small
enough to result in a 'row' value like 0.00045? I'm not sure if we have
"bigger" values (e.g 1074(ntuples) / 117(nloops) which would result in 9.17
rows) this would also be true, what do you think? If you could provide
an example of this case would be great!Based on what was discussed earlier in the thread, there are cases with
large loops [0]. However, I believe it's better not to display average
rows with excessively long digits or in scientific notation. And, of
course, I agree with you regarding small values. I think we should also
add a check to ensure that the total rows is actually greater than zero.
When the total rows is zero, we could simply display it as an integer
without decimals. It could help users average rows is very small but not
zero. What do you think about this approach?Yeah, I agree with you about the long digits. My question is more about why do
we need the planstate->instrument->ntuples < nloops check? I tried to remove
this check and I got a lot of EXPLAIN output that shows 'rows' values with .00,
so I'm just trying to understand the reason. From what I've understood about
this thread is that just avoiding .00 decimals of 'rows' values that could be
just integers would be enough, is that right or I'm missing something here? I'm
just worried if we could have a scenario where nloops > 1 &&
planstate->instrument->ntuples < nloops which would make the 'rows' not be
formatted correctly.
Sorry for missing your question earlier. If you notice in the code
above, the variable(average) 'rows' is defined as:
double rows = planstate->instrument->ntuples / nloops;
This represents the total rows divided by the number of loops. The
condition means that variable 'rows' will always between zero and one.
Therefore, the average rows under such conditions cannot be greater than
or even equal to one. I wrote this condition specifically to avoid the
verbose expression 'rows > 0 && rows < 1'. However, since this might not
be obvious to everyone, perhaps it'd be better to write is using 'rows'
directly or add a comment explaining this logic.
- executing the index scans on <literal>tenk2</literal>. + executing the index scans on <literal>tenk2</literal>. If a subplan node + is executed multiple times and the average number of rows is less than one, + the rows and <literal>loops</literal> values are shown as a decimal fraction + (with two digits after the decimal point) to indicate that some rows + were actually processed rather than simply rounding down to zero.* I think that it would be good to mention what a 'row' value in
decimal means. For
example, if its says "0.1 rows" the user should assume that typically 0 rows
will be returned but sometimes it can return 1 or more.* There are more spaces than necessary before "If a subplan node ..."
* Maybe wrap 'rows' with <literal> </literal>?
I agree with the last two points. As for the first one—maybe we could
simply state that the average rows value can be decimal, especially for
very small values?I'm just not sure about the "small values"; the 'rows' in decimal will only
happen with small values? What would be a "small value" in this context? My main
point here is more that I think that it would be good to mention *why* the
'rows' can be decimal, not just describe that it could be decimal.
As for 'small values', it means that the average rows is between zero
and one, to avoid rounding errors and misunderstanding. I think this
would be ideal.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Em seg., 10 de fev. de 2025 às 18:14, Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> escreveu:
Sorry for missing your question earlier. If you notice in the code above, the variable(average) 'rows' is defined as:
double rows = planstate->instrument->ntuples / nloops;
This represents the total rows divided by the number of loops. The condition
means that variable 'rows' will always between zero and one. Therefore, the
average rows under such conditions cannot be greater than or even equal to
one. I wrote this condition specifically to avoid the verbose expression
'rows > 0 && rows < 1'. However, since this might not be obvious to everyone,
perhaps it'd be better to write is using 'rows' directly or add a comment
explaining this logic.
Thanks for the details! It makes sense to me now. I think that adding a comment
could be a good idea
I agree with the last two points. As for the first one—maybe we could
simply state that the average rows value can be decimal, especially for
very small values?I'm just not sure about the "small values"; the 'rows' in decimal will only
happen with small values? What would be a "small value" in this context? My main
point here is more that I think that it would be good to mention *why* the
'rows' can be decimal, not just describe that it could be decimal.As for 'small values', it means that the average rows is between zero and
one, to avoid rounding errors and misunderstanding. I think this would be
ideal.
Get it, sounds reasonable to me.
--
Matheus Alcantara
On 8/2/2025 04:28, Ilia Evdokimov wrote:
On 08.02.2025 00:01, Matheus Alcantara wrote:
Just for reference I'm trying to apply based on commit fb056564ec5.
You are right, because two commits were appeared after creating v6-patch
on partition_prune.out and patch v6 must not have applied on master.
Then I created v7 patch rebased on fb056564ec5 . Thank for your remark!
I support the idea in general, but I believe it should be expanded to
cover all cases of parameterised plan nodes. Each rescan iteration may
produce a different number of tuples, and rounding can obscure important
data.
For example, consider five loops of a scan node: the first loop returns
nine tuples, and each other - zero tuples. When we calculate the
average, 9 divided by 5 equals 1.8. This results in an explanation that
indicates "rows = 1," masking almost 40% of the data.
Now, if we apply the same two loops but have a total of 900,000 tuples,
then 400,000 masked tuples represent a significant portion of the data.
Moreover, switching to a floating-point type for row explanations in
each parameterised node would provide a more comprehensive view and add
valuable information about the parameterisation of the node, which may
not be immediately apparent.
--
regards, Andrei Lepikhov
On Tue, Feb 11, 2025 at 12:14 PM Andrei Lepikhov <lepihov@gmail.com> wrote:
I support the idea in general, but I believe it should be expanded to
cover all cases of parameterised plan nodes. Each rescan iteration may
produce a different number of tuples, and rounding can obscure important
data.For example, consider five loops of a scan node: the first loop returns
nine tuples, and each other - zero tuples. When we calculate the
average, 9 divided by 5 equals 1.8. This results in an explanation that
indicates "rows = 1," masking almost 40% of the data.Now, if we apply the same two loops but have a total of 900,000 tuples,
then 400,000 masked tuples represent a significant portion of the data.Moreover, switching to a floating-point type for row explanations in
each parameterised node would provide a more comprehensive view and add
valuable information about the parameterisation of the node, which may
not be immediately apparent.
I agree strongly with all of this. I believe we should just implement
what was agreed here:
/messages/by-id/21013.1243618236@sss.pgh.pa.us
Let's just display 2 fractional digits when nloops>1, else 0, and call it good.
--
Robert Haas
EDB: http://www.enterprisedb.com
On 11.02.2025 20:41, Robert Haas wrote:
On Tue, Feb 11, 2025 at 12:14 PM Andrei Lepikhov <lepihov@gmail.com> wrote:
I support the idea in general, but I believe it should be expanded to
cover all cases of parameterised plan nodes. Each rescan iteration may
produce a different number of tuples, and rounding can obscure important
data.For example, consider five loops of a scan node: the first loop returns
nine tuples, and each other - zero tuples. When we calculate the
average, 9 divided by 5 equals 1.8. This results in an explanation that
indicates "rows = 1," masking almost 40% of the data.Now, if we apply the same two loops but have a total of 900,000 tuples,
then 400,000 masked tuples represent a significant portion of the data.Moreover, switching to a floating-point type for row explanations in
each parameterised node would provide a more comprehensive view and add
valuable information about the parameterisation of the node, which may
not be immediately apparent.I agree strongly with all of this. I believe we should just implement
what was agreed here:/messages/by-id/21013.1243618236@sss.pgh.pa.us
Let's just display 2 fractional digits when nloops>1, else 0, and call it good.
Thank you for your review!
With such example, it's hard to disagree with it. This would really add
valuable information. Taking all opinions into account, I have updated
the patch v8. I have also included a check for the case where there are
only zeros after the decimal point. We do not want to clutter the rows
with unnecessary zeros.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v8-0001-Clarify-display-of-rows-as-decimal-fractions.patchtext/x-patch; charset=UTF-8; name=v8-0001-Clarify-display-of-rows-as-decimal-fractions.patchDownload
From e1a46d3fa5a4d20b6ea4728b2a3955f375539295 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Date: Tue, 11 Feb 2025 22:12:41 +0300
Subject: [PATCH] Clarify display of rows as decimal fractions
When loops > 1, the average rows value is shown as decimal fractions
with two digits after the decimal point in EXPLAIN ANALYZE.
---
doc/src/sgml/perform.sgml | 2 +
src/backend/commands/explain.c | 55 +++++++++++++------
src/test/regress/expected/partition_prune.out | 18 +++---
src/test/regress/sql/partition_prune.sql | 2 +-
4 files changed, 51 insertions(+), 26 deletions(-)
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aaba..a01832040c 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -758,6 +758,8 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
the <literal>loops</literal> value to get the total time actually spent in
the node. In the above example, we spent a total of 0.030 milliseconds
executing the index scans on <literal>tenk2</literal>.
+ If <literal>loops</literal> is greater than 1,
+ the <literal>rows</literal> is shown as a decimal unless it's an integer.
</para>
<para>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82e..ae81aeea13 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,8 @@
*/
#include "postgres.h"
+#include <math.h>
+
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/createas.h"
@@ -69,6 +71,9 @@ typedef struct SerializeMetrics
*/
#define BYTES_TO_KILOBYTES(b) (((b) + 1023) / 1024)
+/* Check if float/double has any decimal number */
+#define HAS_DECIMAL(x) (floor(x) != x)
+
static void ExplainOneQuery(Query *query, int cursorOptions,
IntoClause *into, ExplainState *es,
ParseState *pstate, ParamListInfo params);
@@ -1981,14 +1986,15 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
+ appendStringInfo(es->str, " (actual ");
+
if (es->timing)
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, "time=%.3f..%.3f ", startup_ms, total_ms);
+
+ if (nloops > 1 && HAS_DECIMAL(rows))
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f)", rows, nloops);
else
- appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ appendStringInfo(es->str, "rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -1999,8 +2005,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ if (nloops > 1 && HAS_DECIMAL(rows))
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
}
else if (es->analyze)
@@ -2052,14 +2066,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
+ appendStringInfo(es->str, "actual ");
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, "time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && HAS_DECIMAL(rows))
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f\n", rows, nloops);
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ appendStringInfo(es->str, "rows=%.0f loops=%.0f\n", rows, nloops);
}
else
{
@@ -2070,8 +2084,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+ if (nloops > 1 && HAS_DECIMAL(rows))
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index e667503c96..fde44db1a0 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2367,7 +2367,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
@@ -3049,14 +3049,14 @@ order by tbl1.col1, tprt.col1;
insert into tbl1 values (1001), (1010), (1011);
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=23 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=5 loops=5)
+ -> Append (actual rows=4.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
Index Cond: (col1 < tbl1.col1)
- -> Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
+ -> Index Scan using tprt2_idx on tprt_2 (actual rows=2.75 loops=4)
Index Cond: (col1 < tbl1.col1)
-> Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
Index Cond: (col1 < tbl1.col1)
@@ -3070,16 +3070,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 730545e86a..58756e0b18 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -586,7 +586,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
--
2.34.1
Hi! Thank you for your valuable work on this!
On 11.02.2025 22:18, Ilia Evdokimov wrote:
On 11.02.2025 20:41, Robert Haas wrote:
On Tue, Feb 11, 2025 at 12:14 PM Andrei Lepikhov <lepihov@gmail.com>
wrote:I support the idea in general, but I believe it should be expanded to
cover all cases of parameterised plan nodes. Each rescan iteration may
produce a different number of tuples, and rounding can obscure
important
data.For example, consider five loops of a scan node: the first loop returns
nine tuples, and each other - zero tuples. When we calculate the
average, 9 divided by 5 equals 1.8. This results in an explanation that
indicates "rows = 1," masking almost 40% of the data.Now, if we apply the same two loops but have a total of 900,000 tuples,
then 400,000 masked tuples represent a significant portion of the data.Moreover, switching to a floating-point type for row explanations in
each parameterised node would provide a more comprehensive view and add
valuable information about the parameterisation of the node, which may
not be immediately apparent.I agree strongly with all of this. I believe we should just implement
what was agreed here:/messages/by-id/21013.1243618236@sss.pgh.pa.us
Let's just display 2 fractional digits when nloops>1, else 0, and
call it good.Thank you for your review!
With such example, it's hard to disagree with it. This would really
add valuable information. Taking all opinions into account, I have
updated the patch v8. I have also included a check for the case where
there are only zeros after the decimal point. We do not want to
clutter the rows with unnecessary zeros.
I looked at the patch and agree with them. I would suggest adding a
description of how this can help in analyzing the query plans -
I think there is a lack of a description of the reason why this is done
in the commit message.
I would also add the same to the documentation with an example.
--
Regards,
Alena Rybakina
Postgres Professional
On 12/2/2025 00:41, Robert Haas wrote:
On Tue, Feb 11, 2025 at 12:14 PM Andrei Lepikhov <lepihov@gmail.com> wrote:
Moreover, switching to a floating-point type for row explanations in
each parameterised node would provide a more comprehensive view and add
valuable information about the parameterisation of the node, which may
not be immediately apparent.I agree strongly with all of this. I believe we should just implement
what was agreed here:/messages/by-id/21013.1243618236@sss.pgh.pa.us
Let's just display 2 fractional digits when nloops>1, else 0, and call it good.
Why are there only two fractional digits?
I reviewed the user reports where we identified issues without
sufficient data, based on explains only, and typical examples included
loop numbers ranging from 1E5 to 1E7 and tuples from 1E2 to 1E5.
Therefore, it may make sense to display fractional digits up to two
meaningful (non-zero) digits.
--
regards, Andrei Lepikhov
Robert Haas <robertmhaas@gmail.com> writes:
On Tue, Feb 11, 2025 at 12:14 PM Andrei Lepikhov <lepihov@gmail.com> wrote:
I support the idea in general, but I believe it should be expanded to
cover all cases of parameterised plan nodes. Each rescan iteration may
produce a different number of tuples, and rounding can obscure important
data.
I agree strongly with all of this. I believe we should just implement
what was agreed here:
/messages/by-id/21013.1243618236@sss.pgh.pa.us
Let's just display 2 fractional digits when nloops>1, else 0, and call it good.
Note that that formulation has nothing especially to do with
parameterized plan nodes. Any nestloop inner side would end up
getting shown with fractional rowcounts. Maybe that's fine.
I suggest that when thinking about what to change here,
you start by considering how you'd adjust the docs at
https://www.postgresql.org/docs/devel/using-explain.html
to explain the new behavior. If you can't explain it
clearly for users, then maybe it's not such a great idea.
regards, tom lane
On 12/2/2025 03:46, Tom Lane wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Tue, Feb 11, 2025 at 12:14 PM Andrei Lepikhov <lepihov@gmail.com> wrote:
I support the idea in general, but I believe it should be expanded to
cover all cases of parameterised plan nodes. Each rescan iteration may
produce a different number of tuples, and rounding can obscure important
data.I agree strongly with all of this. I believe we should just implement
what was agreed here:
/messages/by-id/21013.1243618236@sss.pgh.pa.us
Let's just display 2 fractional digits when nloops>1, else 0, and call it good.Note that that formulation has nothing especially to do with
parameterized plan nodes. Any nestloop inner side would end up
getting shown with fractional rowcounts. Maybe that's fine.
I partly agree with this approach. Playing around a bit, I couldn't
invent a case where we have different numbers of tuples without
parameters. But I can imagine it is possible or may be possible in
future. So, it is not necessary to tangle fractional output with a
parameterised node.
I'm unsure about the inner subtree of a JOIN - subplan may refer to the
upper query and process a different number of tuples for every
evaluation without any JOIN operator.
May we agree on a more general formula to print at least two meaningful
digits if we have a fractional part?
Examples:
- actual rows = 2, nloops = 2 -> rows = 1
- actual rows = 9, nloops = 5 -> rows = 1.8
- actual rows = 101, nloops = 100 -> rows = 1.0
- actual rows = 99, nloops = 1000000 -> rows = 0.000099
It may guarantee that an EXPLAIN exposes most of the data passed the
node, enough to tangle it with actual timing and tuples at the upper
levels of the query.
I suggest that when thinking about what to change here,
you start by considering how you'd adjust the docs at
https://www.postgresql.org/docs/devel/using-explain.html
to explain the new behavior. If you can't explain it
clearly for users, then maybe it's not such a great idea.
Agree
--
regards, Andrei Lepikhov
On 12.02.2025 11:54, Andrei Lepikhov wrote:
May we agree on a more general formula to print at least two
meaningful digits if we have a fractional part?Examples:
- actual rows = 2, nloops = 2 -> rows = 1
- actual rows = 9, nloops = 5 -> rows = 1.8
- actual rows = 101, nloops = 100 -> rows = 1.0
- actual rows = 99, nloops = 1000000 -> rows = 0.000099It may guarantee that an EXPLAIN exposes most of the data passed the
node, enough to tangle it with actual timing and tuples at the upper
levels of the query.
I think the idea of keeping two significant digits after the decimal
point is quite reasonable. The thing is, rows=0.000001 or something
similar can only occur when loops is quite large. If we show the order
of magnitude in rows, it will be easier for the user to estimate the
order of total rows. For example, if we see this:
rows=0.000056 loops=4718040
the user can quickler approximate the order of total rows for analyzing
the upper levels of the query.
However, keep in mind that I am against using the E notation, as many
users have mentioned that they are not mathematicians and are not
familiar with the concept of "E".
I suggest that when thinking about what to change here,
you start by considering how you'd adjust the docs at
https://www.postgresql.org/docs/devel/using-explain.html
to explain the new behavior. If you can't explain it
clearly for users, then maybe it's not such a great idea.Agree
So do I. Firstly, I'll think how to explain it.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
On Tue, Feb 11, 2025 at 2:18 PM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:
With such example, it's hard to disagree with it. This would really add
valuable information. Taking all opinions into account, I have updated
the patch v8. I have also included a check for the case where there are
only zeros after the decimal point. We do not want to clutter the rows
with unnecessary zeros.
I disagree. We don't do this for any other fractional value we print
in any other part of the system, and I do not think this case should
be some kind of weird exception.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Wed, Feb 12, 2025 at 5:10 AM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:
I think the idea of keeping two significant digits after the decimal
point is quite reasonable. The thing is, rows=0.000001 or something
similar can only occur when loops is quite large. If we show the order
of magnitude in rows, it will be easier for the user to estimate the
order of total rows. For example, if we see this:rows=0.000056 loops=4718040
the user can quickler approximate the order of total rows for analyzing
the upper levels of the query.
I agree that showing 2 digits after the decimal point in all cases is
not ideal, but I suggest that we take a practical approach. Figuring
out dynamically what number of decimal digits to display in each case
sounds complicated and we may spend a bunch of time arguing about the
details of that and get nothing committed. If we just show 2 digits
after the decimal point, it will not be perfect, but it will be 10^2
times better than what we have now.
If I'm honest, what I actually think we should do is stop dividing
values by nloops before printing them out. Every time I'm looking at a
quantity that has been divided by nloops, the very first thing I do is
try to figure out what the original value was. The whole reason I want
to display at least a couple of decimal digits here is so that I can
do that more accurately, but of course the dream would be not having
to reverse engineer it like that at all. However, I expect fierce
opposition to that idea, and no matter how misguided I may think that
opposition might be, a patch in the tree is worth two in the
CommitFest.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
I agree that showing 2 digits after the decimal point in all cases is
not ideal, but I suggest that we take a practical approach. Figuring
out dynamically what number of decimal digits to display in each case
sounds complicated and we may spend a bunch of time arguing about the
details of that and get nothing committed. If we just show 2 digits
after the decimal point, it will not be perfect, but it will be 10^2
times better than what we have now.
I was idly speculating yesterday about letting the Ryu code print
the division result, so that we get a variable number of digits.
Realistically, that'd probably result in many cases in more digits
than anybody wants, so it's not a serious proposal. I'm cool with
the fixed-two-digits approach to start with.
regards, tom lane
On 13/2/2025 01:40, Tom Lane wrote:
I was idly speculating yesterday about letting the Ryu code print
the division result, so that we get a variable number of digits.
Realistically, that'd probably result in many cases in more digits
than anybody wants, so it's not a serious proposal. I'm cool with
the fixed-two-digits approach to start with.
Okay, since no one else voted for the meaningful-numbers approach, I
would say that fixed size is better than nothing. It may cover some of
my practical cases, but unfortunately, not the most problematic ones.
--
regards, Andrei Lepikhov
On Wed, Feb 12, 2025 at 2:55 PM Andrei Lepikhov <lepihov@gmail.com> wrote:
On 13/2/2025 01:40, Tom Lane wrote:
I was idly speculating yesterday about letting the Ryu code print
the division result, so that we get a variable number of digits.
Realistically, that'd probably result in many cases in more digits
than anybody wants, so it's not a serious proposal. I'm cool with
the fixed-two-digits approach to start with.Okay, since no one else voted for the meaningful-numbers approach, I
would say that fixed size is better than nothing. It may cover some of
my practical cases, but unfortunately, not the most problematic ones.
I don't love it either, but I do think it is significantly better than nothing.
--
Robert Haas
EDB: http://www.enterprisedb.com
On 12.02.2025 22:56, Robert Haas wrote:
On Wed, Feb 12, 2025 at 2:55 PM Andrei Lepikhov<lepihov@gmail.com> wrote:
On 13/2/2025 01:40, Tom Lane wrote:
I was idly speculating yesterday about letting the Ryu code print
the division result, so that we get a variable number of digits.
Realistically, that'd probably result in many cases in more digits
than anybody wants, so it's not a serious proposal. I'm cool with
the fixed-two-digits approach to start with.Okay, since no one else voted for the meaningful-numbers approach, I
would say that fixed size is better than nothing. It may cover some of
my practical cases, but unfortunately, not the most problematic ones.I don't love it either, but I do think it is significantly better than nothing.
I'm in favor of having some improvement rather than nothing at
all—otherwise, we might never reach a consensus.
1. Documentation
(v9-0001-Clarify-display-of-rows-as-decimal-fractions-DOC.patch)
One thing that bothers me is that the documentation explains how to
compute total time, but it does not clarify how to compute total rows.
Maybe this was obvious to others before, but now that we are displaying
|rows| as a fraction, we should explicitly document how to interpret it
alongside total time.
I believe it would be helpful to show a non-integer rows value in an
example query. However, to achieve this, we need the index scan results
to vary across iterations. One way to accomplish this is by using the
condition t1.unique2 > t2.unique2. Additionally, I suggest making loops
a round number (e.g., 100) for better readability, which can be achieved
using t1.thousand < 10. The final query would look like this:
EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.thousand < 10 AND t1.unique2 > t2.unique2;
I believe this is an ideal example for the documentation because it not
only demonstrates fractional rows, but also keeps the execution plan
nearly unchanged. While the estimated and actual average row counts
become slightly rounded, I don't see another way to ensure different
results for each index scan.
I'm open to any feedback or suggestions for a better example to use in
the documentation or additional explaining fractional rows in the text.
2. Code and tests
(v9-0002-Clarify-display-of-rows-as-decimal-fractions.patch)
I left the code and tests unchanged since we agreed on a fixed format of
two decimal places.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v9-0001-Clarify-display-of-rows-as-decimal-fractions-DOC.patchtext/x-patch; charset=UTF-8; name=v9-0001-Clarify-display-of-rows-as-decimal-fractions-DOC.patchDownload
From 789d98383988ef6d3b0bc2c6b9b5c73a83ffd6d4 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Thu, 13 Feb 2025 11:19:03 +0300
Subject: [PATCH v9] Clarify display of rows as decimal fractions
When loops > 1, the average rows value is now displayed as a decimal fraction
with two digits after the decimal point in EXPLAIN ANALYZE.
Previously, the average rows value was always rounded to an integer,
which could lead to misleading results when estimating total rows
by multiplying rows by loop. This change ensures that users
get a more accurate representation of the data flow through execution nodes.
---
doc/src/sgml/perform.sgml | 41 ++++++++++++++++++++-------------------
1 file changed, 21 insertions(+), 20 deletions(-)
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aaba..dd61ae4507 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -717,26 +717,26 @@ FROM tenk1 t1 WHERE t1.ten = (SELECT (random() * 10)::integer);
<screen>
EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
-WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
-
- QUERY PLAN
--------------------------------------------------------------------&zwsp;--------------------------------------------------------------
- Nested Loop (cost=4.65..118.50 rows=10 width=488) (actual time=0.017..0.051 rows=10 loops=1)
- Buffers: shared hit=36 read=6
- -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10 loops=1)
- Recheck Cond: (unique1 < 10)
- Heap Blocks: exact=10
- Buffers: shared hit=3 read=5 written=4
- -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
- Index Cond: (unique1 < 10)
+WHERE t1.thousand < 10 AND t1.unique2 > t2.unique2;
+
+ QUERY PLAN
+-------------------------------------------------------------------&zwsp;-------------------------------------------------------------------------
+ Nested Loop (cost=5.40..11571.44 rows=356667 width=488) (actual time=0.042..117.205 rows=513832 loops=1)
+ Buffers: shared hit=19377 read=29
+ -> Bitmap Heap Scan on tenk1 t1 (cost=5.11..233.60 rows=107 width=244) (actual time=0.021..0.103 rows=100 loops=1)
+ Recheck Cond: (thousand < 10)
+ Heap Blocks: exact=90
+ Buffers: shared hit=92
+ -> Bitmap Index Scan on tenk1_thous_tenthous (cost=0.00..5.09 rows=107 width=0) (actual time=0.010..0.010 rows=100 loops=1)
+ Index Cond: (thousand < 10)
Buffers: shared hit=2
- -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
- Index Cond: (unique2 = t1.unique2)
- Buffers: shared hit=24 read=6
+ -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..72.63 rows=3333 width=244) (actual time=0.006..0.410 rows=5138.32 loops=100)
+ Index Cond: (unique2 < t1.unique2)
+ Buffers: shared hit=19285 read=29
Planning:
- Buffers: shared hit=15 dirtied=9
- Planning Time: 0.485 ms
- Execution Time: 0.073 ms
+ Buffers: shared hit=258 read=9 dirtied=2
+ Planning Time: 0.519 ms
+ Execution Time: 131.378 ms
</screen>
Note that the <quote>actual time</quote> values are in milliseconds of
@@ -756,8 +756,9 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
values shown are averages per-execution. This is done to make the numbers
comparable with the way that the cost estimates are shown. Multiply by
the <literal>loops</literal> value to get the total time actually spent in
- the node. In the above example, we spent a total of 0.030 milliseconds
- executing the index scans on <literal>tenk2</literal>.
+ the node, and to get the total rows processed in the node. In the above example,
+ we spent a total of 41 milliseconds executing the index scans,
+ and the node processed a total of 513832 rows on <literal>tenk2</literal>.
</para>
<para>
--
2.34.1
v9-0002-Clarify-display-of-rows-as-decimal-fractions.patchtext/x-patch; charset=UTF-8; name=v9-0002-Clarify-display-of-rows-as-decimal-fractions.patchDownload
From a30ee71240d731c386f9dd4eb37b2817e3238807 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Thu, 13 Feb 2025 11:24:03 +0300
Subject: [PATCH v9] Clarify display of rows as decimal fractions
When loops > 1, the average rows value is now displayed as a decimal fraction
with two digits after the decimal point in EXPLAIN ANALYZE.
Previously, the average rows value was always rounded to an integer,
which could lead to misleading results when estimating total rows
by multiplying rows by loop. This change ensures that users
get a more accurate representation of the data flow through execution nodes.
---
src/backend/commands/explain.c | 55 +++++++++++++------
src/test/regress/expected/partition_prune.out | 18 +++---
src/test/regress/sql/partition_prune.sql | 2 +-
3 files changed, 49 insertions(+), 26 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82e..ae81aeea13 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -13,6 +13,8 @@
*/
#include "postgres.h"
+#include <math.h>
+
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/createas.h"
@@ -69,6 +71,9 @@ typedef struct SerializeMetrics
*/
#define BYTES_TO_KILOBYTES(b) (((b) + 1023) / 1024)
+/* Check if float/double has any decimal number */
+#define HAS_DECIMAL(x) (floor(x) != x)
+
static void ExplainOneQuery(Query *query, int cursorOptions,
IntoClause *into, ExplainState *es,
ParseState *pstate, ParamListInfo params);
@@ -1981,14 +1986,15 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
+ appendStringInfo(es->str, " (actual ");
+
if (es->timing)
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, "time=%.3f..%.3f ", startup_ms, total_ms);
+
+ if (nloops > 1 && HAS_DECIMAL(rows))
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f)", rows, nloops);
else
- appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ appendStringInfo(es->str, "rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -1999,8 +2005,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ if (nloops > 1 && HAS_DECIMAL(rows))
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
}
else if (es->analyze)
@@ -2052,14 +2066,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
+ appendStringInfo(es->str, "actual ");
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, "time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && HAS_DECIMAL(rows))
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f\n", rows, nloops);
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ appendStringInfo(es->str, "rows=%.0f loops=%.0f\n", rows, nloops);
}
else
{
@@ -2070,8 +2084,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+ if (nloops > 1 && HAS_DECIMAL(rows))
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index e667503c96..fde44db1a0 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2367,7 +2367,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
@@ -3049,14 +3049,14 @@ order by tbl1.col1, tprt.col1;
insert into tbl1 values (1001), (1010), (1011);
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=23 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=5 loops=5)
+ -> Append (actual rows=4.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
Index Cond: (col1 < tbl1.col1)
- -> Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
+ -> Index Scan using tprt2_idx on tprt_2 (actual rows=2.75 loops=4)
Index Cond: (col1 < tbl1.col1)
-> Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
Index Cond: (col1 < tbl1.col1)
@@ -3070,16 +3070,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 730545e86a..58756e0b18 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -586,7 +586,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
--
2.34.1
On Thu, Feb 13, 2025 at 4:05 AM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:
1. Documentation (v9-0001-Clarify-display-of-rows-as-decimal-fractions-DOC.patch)
One thing that bothers me is that the documentation explains how to compute total time, but it does not clarify how to compute total rows. Maybe this was obvious to others before, but now that we are displaying rows as a fraction, we should explicitly document how to interpret it alongside total time.
I believe it would be helpful to show a non-integer rows value in an example query. However, to achieve this, we need the index scan results to vary across iterations. One way to accomplish this is by using the condition t1.unique2 > t2.unique2. Additionally, I suggest making loops a round number (e.g., 100) for better readability, which can be achieved using t1.thousand < 10. The final query would look like this:
EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.thousand < 10 AND t1.unique2 > t2.unique2;I believe this is an ideal example for the documentation because it not only demonstrates fractional rows, but also keeps the execution plan nearly unchanged. While the estimated and actual average row counts become slightly rounded, I don't see another way to ensure different results for each index scan.
Anyone else have an opinion on this? I see Ilia's point, but a
non-equality join is probably an atypical case.
2. Code and tests (v9-0002-Clarify-display-of-rows-as-decimal-fractions.patch)
I left the code and tests unchanged since we agreed on a fixed format of two decimal places.
This still has the HAS_DECIMAL() thing to which I objected.
--
Robert Haas
EDB: http://www.enterprisedb.com
On 13/2/2025 21:42, Robert Haas wrote:
On Thu, Feb 13, 2025 at 4:05 AM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:1. Documentation (v9-0001-Clarify-display-of-rows-as-decimal-fractions-DOC.patch)
One thing that bothers me is that the documentation explains how to compute total time, but it does not clarify how to compute total rows. Maybe this was obvious to others before, but now that we are displaying rows as a fraction, we should explicitly document how to interpret it alongside total time.
I believe it would be helpful to show a non-integer rows value in an example query. However, to achieve this, we need the index scan results to vary across iterations. One way to accomplish this is by using the condition t1.unique2 > t2.unique2. Additionally, I suggest making loops a round number (e.g., 100) for better readability, which can be achieved using t1.thousand < 10. The final query would look like this:
EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.thousand < 10 AND t1.unique2 > t2.unique2;I believe this is an ideal example for the documentation because it not only demonstrates fractional rows, but also keeps the execution plan nearly unchanged. While the estimated and actual average row counts become slightly rounded, I don't see another way to ensure different results for each index scan.
Anyone else have an opinion on this? I see Ilia's point, but a
non-equality join is probably an atypical case.
For me, it is ok: it demonstrates the feature and may legally happen.
But this patch should be the second one, isn't it?
2. Code and tests (v9-0002-Clarify-display-of-rows-as-decimal-fractions.patch)
I left the code and tests unchanged since we agreed on a fixed format of two decimal places.
This still has the HAS_DECIMAL() thing to which I objected.
I don't understand why using math.h and the floor() routine is
necessary. I personally prefer x%y!=0 expression.
--
regards, Andrei Lepikhov
On 15.02.2025 11:46, Andrei Lepikhov wrote:
On 13/2/2025 21:42, Robert Haas wrote:
On Thu, Feb 13, 2025 at 4:05 AM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:1. Documentation
(v9-0001-Clarify-display-of-rows-as-decimal-fractions-DOC.patch)One thing that bothers me is that the documentation explains how to
compute total time, but it does not clarify how to compute total
rows. Maybe this was obvious to others before, but now that we are
displaying rows as a fraction, we should explicitly document how to
interpret it alongside total time.I believe it would be helpful to show a non-integer rows value in an
example query. However, to achieve this, we need the index scan
results to vary across iterations. One way to accomplish this is by
using the condition t1.unique2 > t2.unique2. Additionally, I suggest
making loops a round number (e.g., 100) for better readability,
which can be achieved using t1.thousand < 10. The final query would
look like this:EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.thousand < 10 AND t1.unique2 > t2.unique2;I believe this is an ideal example for the documentation because it
not only demonstrates fractional rows, but also keeps the execution
plan nearly unchanged. While the estimated and actual average row
counts become slightly rounded, I don't see another way to ensure
different results for each index scan.Anyone else have an opinion on this? I see Ilia's point, but a
non-equality join is probably an atypical case.For me, it is ok: it demonstrates the feature and may legally happen.
But this patch should be the second one, isn't it?
I didn't want to introduce a new table instead of tenk1 and tenk2, as
the entire page already uses them. Moreover, if we use = in the join
condition, the number of rows will always be the same. So, I couldn't
find a better option than t1.unique > t2.unique.
Regarding the order of the patches, I made this one first because, as
Tom mentioned earlier, we need to finalize the documentation first.
However, in order to check tests in CirrusCI, it should be really the
second.
2. Code and tests
(v9-0002-Clarify-display-of-rows-as-decimal-fractions.patch)I left the code and tests unchanged since we agreed on a fixed
format of two decimal places.This still has the HAS_DECIMAL() thing to which I objected.
I don't understand why using math.h and the floor() routine is
necessary. I personally prefer x%y!=0 expression.
You're right—floor shouldn't be used since it behaves differently across
platforms, as Tom also pointed out earlier. I like the idea of using %,
but since the compiler doesn't allow this operation with double, we need
to cast it to int64. I checked how nloops and ntuples are modified -
they are only incremented by 1. So that the casting might be safe. If I
missed anything or if there's a reason why this casting wouldn't be
safe, please let me know.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v10-0002-Clarify-display-of-rows-as-decimal-fractions-DOC.patchtext/x-patch; charset=UTF-8; name=v10-0002-Clarify-display-of-rows-as-decimal-fractions-DOC.patchDownload
From c1494f8815b0e69479d19e74e10970a54c8e6de6 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Mon, 17 Feb 2025 11:02:33 +0300
Subject: [PATCH v10 2/2] Clarify display of rows as decimal fractions
When loops > 1, the average rows value is now displayed as a decimal fraction
with two digits after the decimal point in EXPLAIN ANALYZE.
Previously, the average rows value was always rounded to an integer,
which could lead to misleading results when estimating total rows
by multiplying rows by loop. This change ensures that users
get a more accurate representation of the data flow through execution nodes.
---
doc/src/sgml/perform.sgml | 39 ++++++++++++++++++++-------------------
1 file changed, 20 insertions(+), 19 deletions(-)
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aaba..588ee6d5aa 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -717,26 +717,26 @@ FROM tenk1 t1 WHERE t1.ten = (SELECT (random() * 10)::integer);
<screen>
EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
-WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
+WHERE t1.thousand < 10 AND t1.unique2 > t2.unique2;
- QUERY PLAN
--------------------------------------------------------------------&zwsp;--------------------------------------------------------------
- Nested Loop (cost=4.65..118.50 rows=10 width=488) (actual time=0.017..0.051 rows=10 loops=1)
- Buffers: shared hit=36 read=6
- -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10 loops=1)
- Recheck Cond: (unique1 < 10)
- Heap Blocks: exact=10
- Buffers: shared hit=3 read=5 written=4
- -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
- Index Cond: (unique1 < 10)
+ QUERY PLAN
+-------------------------------------------------------------------&zwsp;-------------------------------------------------------------------------
+ Nested Loop (cost=5.40..11571.44 rows=356667 width=488) (actual time=0.042..117.205 rows=513832 loops=1)
+ Buffers: shared hit=19377 read=29
+ -> Bitmap Heap Scan on tenk1 t1 (cost=5.11..233.60 rows=107 width=244) (actual time=0.021..0.103 rows=100 loops=1)
+ Recheck Cond: (thousand < 10)
+ Heap Blocks: exact=90
+ Buffers: shared hit=92
+ -> Bitmap Index Scan on tenk1_thous_tenthous (cost=0.00..5.09 rows=107 width=0) (actual time=0.010..0.010 rows=100 loops=1)
+ Index Cond: (thousand < 10)
Buffers: shared hit=2
- -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
- Index Cond: (unique2 = t1.unique2)
- Buffers: shared hit=24 read=6
+ -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..72.63 rows=3333 width=244) (actual time=0.006..0.410 rows=5138.32 loops=100)
+ Index Cond: (unique2 < t1.unique2)
+ Buffers: shared hit=19285 read=29
Planning:
- Buffers: shared hit=15 dirtied=9
- Planning Time: 0.485 ms
- Execution Time: 0.073 ms
+ Buffers: shared hit=258 read=9 dirtied=2
+ Planning Time: 0.519 ms
+ Execution Time: 131.378 ms
</screen>
Note that the <quote>actual time</quote> values are in milliseconds of
@@ -756,8 +756,9 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
values shown are averages per-execution. This is done to make the numbers
comparable with the way that the cost estimates are shown. Multiply by
the <literal>loops</literal> value to get the total time actually spent in
- the node. In the above example, we spent a total of 0.030 milliseconds
- executing the index scans on <literal>tenk2</literal>.
+ the node, and to get the total rows processed in the node. In the above example,
+ we spent a total of 41 milliseconds executing the index scans,
+ and the node processed a total of 513832 rows on <literal>tenk2</literal>.
</para>
<para>
--
2.34.1
v10-0001-Clarify-display-of-rows-as-decimal-fractions.patchtext/x-patch; charset=UTF-8; name=v10-0001-Clarify-display-of-rows-as-decimal-fractions.patchDownload
From 0ba7069534afcf54cd39fc4bc21aa44d5bf3e343 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Mon, 17 Feb 2025 10:59:50 +0300
Subject: [PATCH v10 1/2] Clarify display of rows as decimal fractions
When loops > 1, the average rows value is now displayed as a decimal fraction
with two digits after the decimal point in EXPLAIN ANALYZE.
Previously, the average rows value was always rounded to an integer,
which could lead to misleading results when estimating total rows
by multiplying rows by loop. This change ensures that users
get a more accurate representation of the data flow through execution nodes.
---
src/backend/commands/explain.c | 54 +++++++++++++------
src/test/regress/expected/partition_prune.out | 18 +++----
src/test/regress/sql/partition_prune.sql | 2 +-
3 files changed, 48 insertions(+), 26 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index dc4bef9ab8..50b3b224c2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1978,17 +1978,20 @@ 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;
+ bool rows_is_fractonal = (int64)planstate->instrument->ntuples %
+ (int64)nloops;
if (es->format == EXPLAIN_FORMAT_TEXT)
{
+ appendStringInfo(es->str, " (actual ");
+
if (es->timing)
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, "time=%.3f..%.3f ", startup_ms, total_ms);
+
+ if (nloops > 1 && rows_is_fractonal)
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f)", rows, nloops);
else
- appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ appendStringInfo(es->str, "rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -1999,8 +2002,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ if (nloops > 1 && rows_is_fractonal)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
}
else if (es->analyze)
@@ -2040,26 +2051,28 @@ ExplainNode(PlanState *planstate, List *ancestors,
double startup_ms;
double total_ms;
double rows;
+ bool rows_is_fractonal;
if (nloops <= 0)
continue;
startup_ms = 1000.0 * instrument->startup / nloops;
total_ms = 1000.0 * instrument->total / nloops;
rows = instrument->ntuples / nloops;
+ rows_is_fractonal = (int64)instrument->ntuples % (int64)nloops;
ExplainOpenWorker(n, es);
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
+ appendStringInfo(es->str, "actual ");
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, "time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1 && rows_is_fractonal)
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f\n", rows, nloops);
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ appendStringInfo(es->str, "rows=%.0f loops=%.0f\n", rows, nloops);
}
else
{
@@ -2070,8 +2083,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+ if (nloops > 1 && rows_is_fractonal)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 6f80b62a3b..1e9ceee593 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2367,7 +2367,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
@@ -3049,14 +3049,14 @@ order by tbl1.col1, tprt.col1;
insert into tbl1 values (1001), (1010), (1011);
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=23 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=5 loops=5)
+ -> Append (actual rows=4.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
Index Cond: (col1 < tbl1.col1)
- -> Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
+ -> Index Scan using tprt2_idx on tprt_2 (actual rows=2.75 loops=4)
Index Cond: (col1 < tbl1.col1)
-> Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
Index Cond: (col1 < tbl1.col1)
@@ -3070,16 +3070,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 86621dcec0..6aad02156c 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -586,7 +586,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
--
2.34.1
On Mon, Feb 17, 2025 at 3:08 AM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:
You're right—floor shouldn't be used since it behaves differently across
platforms, as Tom also pointed out earlier. I like the idea of using %,
but since the compiler doesn't allow this operation with double, we need
to cast it to int64. I checked how nloops and ntuples are modified -
they are only incremented by 1. So that the casting might be safe. If I
missed anything or if there's a reason why this casting wouldn't be
safe, please let me know.
What I've been advocating for is just:
if (nloops > 1)
Instead of:
if (nloops > 1 && rows_is_fractonal)
I don't think it's really safe to just cast a double back to int64. In
practice, the number of tuples should never be large enough to
overflow int64, but if it did, this result would be nonsense. Also, if
the double ever lost precision, the result would be nonsense. If we
want to have an exact count of tuples, we ought to change ntuples and
ntuples2 to be uint64. But I don't think we should do that in this
patch, because that adds a whole bunch of new problems to worry about
and might cause us to get nothing committed. Instead, I think we
should just always show two decimal digits if there's more than one
loop.
That's simpler than what the patch currently does and avoids this
problem. Perhaps it's objectionable for some other reason, but if so,
can somebody please spell out what that reason is so we can talk about
it?
--
Robert Haas
EDB: http://www.enterprisedb.com
On 17.02.2025 17:19, Robert Haas wrote:
On Mon, Feb 17, 2025 at 3:08 AM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:You're right—floor shouldn't be used since it behaves differently across
platforms, as Tom also pointed out earlier. I like the idea of using %,
but since the compiler doesn't allow this operation with double, we need
to cast it to int64. I checked how nloops and ntuples are modified -
they are only incremented by 1. So that the casting might be safe. If I
missed anything or if there's a reason why this casting wouldn't be
safe, please let me know.What I've been advocating for is just:
if (nloops > 1)
Instead of:
if (nloops > 1 && rows_is_fractonal)
I don't think it's really safe to just cast a double back to int64. In
practice, the number of tuples should never be large enough to
overflow int64, but if it did, this result would be nonsense. Also, if
the double ever lost precision, the result would be nonsense. If we
want to have an exact count of tuples, we ought to change ntuples and
ntuples2 to be uint64. But I don't think we should do that in this
patch, because that adds a whole bunch of new problems to worry about
and might cause us to get nothing committed. Instead, I think we
should just always show two decimal digits if there's more than one
loop.That's simpler than what the patch currently does and avoids this
problem. Perhaps it's objectionable for some other reason, but if so,
can somebody please spell out what that reason is so we can talk about
it?
I have no objections. I agree if we are going to transition from double
to int64, it should be done properly, but definitely not in current thread.
As for documentation changes, I'll keep t1.unique > t2.unique. If we use
equality instead, we need to explicitly show rows=1.00, which would
likely raise too many questions from users what it means.
I attached patches v11 with changes.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v11-0002-Clarify-display-of-rows-as-decimal-fractions.patchtext/x-patch; charset=UTF-8; name=v11-0002-Clarify-display-of-rows-as-decimal-fractions.patchDownload
From 766e8bf46a415cf3c4e3a4c94a2479ea91d3e93f Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Date: Mon, 17 Feb 2025 21:22:11 +0300
Subject: [PATCH v11 2/2] Clarify display of rows as decimal fractions
When loops > 1, the average rows value is now displayed as a decimal fraction
with two digits after the decimal point in EXPLAIN ANALYZE.
Previously, the average rows value was always rounded to an integer,
which could lead to misleading results when estimating total rows
by multiplying rows by loop. This change ensures that users
get a more accurate representation of the data flow through execution nodes.
---
doc/src/sgml/perform.sgml | 39 ++++++++++++++++++++-------------------
1 file changed, 20 insertions(+), 19 deletions(-)
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aaba..588ee6d5aa 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -717,26 +717,26 @@ FROM tenk1 t1 WHERE t1.ten = (SELECT (random() * 10)::integer);
<screen>
EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
-WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
+WHERE t1.thousand < 10 AND t1.unique2 > t2.unique2;
- QUERY PLAN
--------------------------------------------------------------------&zwsp;--------------------------------------------------------------
- Nested Loop (cost=4.65..118.50 rows=10 width=488) (actual time=0.017..0.051 rows=10 loops=1)
- Buffers: shared hit=36 read=6
- -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10 loops=1)
- Recheck Cond: (unique1 < 10)
- Heap Blocks: exact=10
- Buffers: shared hit=3 read=5 written=4
- -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
- Index Cond: (unique1 < 10)
+ QUERY PLAN
+-------------------------------------------------------------------&zwsp;-------------------------------------------------------------------------
+ Nested Loop (cost=5.40..11571.44 rows=356667 width=488) (actual time=0.042..117.205 rows=513832 loops=1)
+ Buffers: shared hit=19377 read=29
+ -> Bitmap Heap Scan on tenk1 t1 (cost=5.11..233.60 rows=107 width=244) (actual time=0.021..0.103 rows=100 loops=1)
+ Recheck Cond: (thousand < 10)
+ Heap Blocks: exact=90
+ Buffers: shared hit=92
+ -> Bitmap Index Scan on tenk1_thous_tenthous (cost=0.00..5.09 rows=107 width=0) (actual time=0.010..0.010 rows=100 loops=1)
+ Index Cond: (thousand < 10)
Buffers: shared hit=2
- -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
- Index Cond: (unique2 = t1.unique2)
- Buffers: shared hit=24 read=6
+ -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..72.63 rows=3333 width=244) (actual time=0.006..0.410 rows=5138.32 loops=100)
+ Index Cond: (unique2 < t1.unique2)
+ Buffers: shared hit=19285 read=29
Planning:
- Buffers: shared hit=15 dirtied=9
- Planning Time: 0.485 ms
- Execution Time: 0.073 ms
+ Buffers: shared hit=258 read=9 dirtied=2
+ Planning Time: 0.519 ms
+ Execution Time: 131.378 ms
</screen>
Note that the <quote>actual time</quote> values are in milliseconds of
@@ -756,8 +756,9 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
values shown are averages per-execution. This is done to make the numbers
comparable with the way that the cost estimates are shown. Multiply by
the <literal>loops</literal> value to get the total time actually spent in
- the node. In the above example, we spent a total of 0.030 milliseconds
- executing the index scans on <literal>tenk2</literal>.
+ the node, and to get the total rows processed in the node. In the above example,
+ we spent a total of 41 milliseconds executing the index scans,
+ and the node processed a total of 513832 rows on <literal>tenk2</literal>.
</para>
<para>
--
2.34.1
v11-0001-Clarify-display-of-rows-as-decimal-fractions.patchtext/x-patch; charset=UTF-8; name=v11-0001-Clarify-display-of-rows-as-decimal-fractions.patchDownload
From 7cb3f36f3fb4db0f75a72df1caa5e3218198fa84 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Date: Mon, 17 Feb 2025 21:20:10 +0300
Subject: [PATCH v11 1/2] Clarify display of rows as decimal fractions
When loops > 1, the average rows value is now displayed as a decimal fraction
with two digits after the decimal point in EXPLAIN ANALYZE.
Previously, the average rows value was always rounded to an integer,
which could lead to misleading results when estimating total rows
by multiplying rows by loop. This change ensures that users
get a more accurate representation of the data flow through execution nodes.
---
src/backend/commands/explain.c | 50 +++++++----
src/test/regress/expected/explain.out | 4 +-
src/test/regress/expected/memoize.out | 86 +++++++++----------
src/test/regress/expected/partition_prune.out | 42 ++++-----
src/test/regress/expected/select_parallel.out | 24 +++---
src/test/regress/sql/partition_prune.sql | 2 +-
6 files changed, 113 insertions(+), 95 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index dc4bef9ab8..ea4ae60d57 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1981,14 +1981,15 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
+ appendStringInfo(es->str, " (actual ");
+
if (es->timing)
- appendStringInfo(es->str,
- " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, "time=%.3f..%.3f ", startup_ms, total_ms);
+
+ if (nloops > 1)
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f)", rows, nloops);
else
- appendStringInfo(es->str,
- " (actual rows=%.0f loops=%.0f)",
- rows, nloops);
+ appendStringInfo(es->str, "rows=%.0f loops=%.0f)", rows, nloops);
}
else
{
@@ -1999,8 +2000,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ if (nloops > 1)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
}
else if (es->analyze)
@@ -2052,14 +2061,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->format == EXPLAIN_FORMAT_TEXT)
{
ExplainIndentText(es);
+ appendStringInfo(es->str, "actual ");
if (es->timing)
- appendStringInfo(es->str,
- "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
- startup_ms, total_ms, rows, nloops);
+ appendStringInfo(es->str, "time=%.3f..%.3f", startup_ms, total_ms);
+
+ if (nloops > 1)
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f\n", rows, nloops);
else
- appendStringInfo(es->str,
- "actual rows=%.0f loops=%.0f\n",
- rows, nloops);
+ appendStringInfo(es->str, "rows=%.0f loops=%.0f\n", rows, nloops);
}
else
{
@@ -2070,8 +2079,17 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms",
total_ms, 3, es);
}
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+ if (nloops > 1)
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ else
+ {
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index ee31e41d50..eb187516b3 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -528,7 +528,7 @@ select jsonb_pretty(
"Plan Rows": 0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
- "Actual Rows": 0, +
+ "Actual Rows": 0.0, +
"Actual Loops": 0, +
"Startup Cost": 0.0, +
"Async Capable": false, +
@@ -575,7 +575,7 @@ select jsonb_pretty(
"Plan Rows": 0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
- "Actual Rows": 0, +
+ "Actual Rows": 0.0, +
"Actual Loops": 0, +
"Startup Cost": 0.0, +
"Async Capable": false, +
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 5ecf971dad..dbd01066d0 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -35,18 +35,18 @@ SELECT explain_memoize('
SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
INNER JOIN tenk1 t2 ON t1.unique1 = t2.twenty
WHERE t2.unique1 < 1000;', false);
- explain_memoize
--------------------------------------------------------------------------------------------
+ explain_memoize
+----------------------------------------------------------------------------------------------
Aggregate (actual rows=1 loops=N)
-> Nested Loop (actual rows=1000 loops=N)
-> Seq Scan on tenk1 t2 (actual rows=1000 loops=N)
Filter: (unique1 < 1000)
Rows Removed by Filter: 9000
- -> Memoize (actual rows=1 loops=N)
+ -> Memoize (actual rows=1.00 loops=N)
Cache Key: t2.twenty
Cache Mode: logical
Hits: 980 Misses: 20 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
+ -> Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1.00 loops=N)
Index Cond: (unique1 = t2.twenty)
Heap Fetches: N
(12 rows)
@@ -66,18 +66,18 @@ SELECT COUNT(*),AVG(t2.unique1) FROM tenk1 t1,
LATERAL (SELECT t2.unique1 FROM tenk1 t2
WHERE t1.twenty = t2.unique1 OFFSET 0) t2
WHERE t1.unique1 < 1000;', false);
- explain_memoize
--------------------------------------------------------------------------------------------
+ explain_memoize
+----------------------------------------------------------------------------------------------
Aggregate (actual rows=1 loops=N)
-> Nested Loop (actual rows=1000 loops=N)
-> Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
Filter: (unique1 < 1000)
Rows Removed by Filter: 9000
- -> Memoize (actual rows=1 loops=N)
+ -> Memoize (actual rows=1.00 loops=N)
Cache Key: t1.twenty
Cache Mode: binary
Hits: 980 Misses: 20 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+ -> Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1.00 loops=N)
Index Cond: (unique1 = t1.twenty)
Heap Fetches: N
(12 rows)
@@ -100,20 +100,20 @@ LATERAL (
) t2
ON t1.two = t2.two
WHERE t1.unique1 < 10;', false);
- explain_memoize
-----------------------------------------------------------------------------------------------
+ explain_memoize
+-------------------------------------------------------------------------------------------------
Aggregate (actual rows=1 loops=N)
-> Nested Loop Left Join (actual rows=20 loops=N)
-> Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
Index Cond: (unique1 < 10)
- -> Memoize (actual rows=2 loops=N)
+ -> Memoize (actual rows=2.00 loops=N)
Cache Key: t1.two
Cache Mode: binary
Hits: 8 Misses: 2 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Subquery Scan on t2 (actual rows=2 loops=N)
+ -> Subquery Scan on t2 (actual rows=2.00 loops=N)
Filter: (t1.two = t2.two)
Rows Removed by Filter: 2
- -> Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4 loops=N)
+ -> Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4.00 loops=N)
Index Cond: (unique1 < 4)
(13 rows)
@@ -134,18 +134,18 @@ SELECT explain_memoize('
SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
LATERAL (SELECT t1.two+1 AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
- explain_memoize
--------------------------------------------------------------------------------------------
+ explain_memoize
+----------------------------------------------------------------------------------------------
Aggregate (actual rows=1 loops=N)
-> Nested Loop (actual rows=1000 loops=N)
-> Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
Filter: (unique1 < 1000)
Rows Removed by Filter: 9000
- -> Memoize (actual rows=1 loops=N)
+ -> Memoize (actual rows=1.00 loops=N)
Cache Key: (t1.two + 1)
Cache Mode: binary
Hits: 998 Misses: 2 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+ -> Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1.00 loops=N)
Filter: ((t1.two + 1) = unique1)
Rows Removed by Filter: 9999
Heap Fetches: N
@@ -173,11 +173,11 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
-> Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
Filter: (unique1 < 1000)
Rows Removed by Filter: 9000
- -> Memoize (actual rows=1 loops=N)
+ -> Memoize (actual rows=1.00 loops=N)
Cache Key: t1.two, t1.twenty
Cache Mode: binary
Hits: 980 Misses: 20 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Seq Scan on tenk1 t2 (actual rows=1 loops=N)
+ -> Seq Scan on tenk1 t2 (actual rows=1.00 loops=N)
Filter: ((t1.twenty = unique1) AND (t1.two = two))
Rows Removed by Filter: 9999
(12 rows)
@@ -207,15 +207,15 @@ VACUUM ANALYZE expr_key;
SELECT explain_memoize('
SELECT * FROM expr_key t1 INNER JOIN expr_key t2
ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
- explain_memoize
--------------------------------------------------------------------------------------------
+ explain_memoize
+----------------------------------------------------------------------------------------------
Nested Loop (actual rows=80 loops=N)
-> Seq Scan on expr_key t1 (actual rows=40 loops=N)
- -> Memoize (actual rows=2 loops=N)
+ -> Memoize (actual rows=2.00 loops=N)
Cache Key: t1.x, (t1.t)::numeric
Cache Mode: logical
Hits: 20 Misses: 20 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2 loops=N)
+ -> Index Only Scan using expr_key_idx_x_t on expr_key t2 (actual rows=2.00 loops=N)
Index Cond: (x = (t1.t)::numeric)
Filter: (t1.x = (t)::numeric)
Heap Fetches: N
@@ -232,18 +232,18 @@ SELECT explain_memoize('
SELECT COUNT(*),AVG(t1.unique1) FROM tenk1 t1
INNER JOIN tenk1 t2 ON t1.unique1 = t2.thousand
WHERE t2.unique1 < 1200;', true);
- explain_memoize
--------------------------------------------------------------------------------------------
+ explain_memoize
+----------------------------------------------------------------------------------------------
Aggregate (actual rows=1 loops=N)
-> Nested Loop (actual rows=1200 loops=N)
-> Seq Scan on tenk1 t2 (actual rows=1200 loops=N)
Filter: (unique1 < 1200)
Rows Removed by Filter: 8800
- -> Memoize (actual rows=1 loops=N)
+ -> Memoize (actual rows=1.00 loops=N)
Cache Key: t2.thousand
Cache Mode: logical
Hits: N Misses: N Evictions: N Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1 loops=N)
+ -> Index Only Scan using tenk1_unique1 on tenk1 t1 (actual rows=1.00 loops=N)
Index Cond: (unique1 = t2.thousand)
Heap Fetches: N
(12 rows)
@@ -261,7 +261,7 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
Nested Loop (actual rows=4 loops=N)
-> Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
Heap Fetches: N
- -> Memoize (actual rows=2 loops=N)
+ -> Memoize (actual rows=2.00 loops=N)
Cache Key: f1.f
Cache Mode: logical
Hits: 1 Misses: 1 Evictions: Zero Overflows: 0 Memory Usage: NkB
@@ -273,16 +273,16 @@ SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
-- Ensure memoize operates in binary mode
SELECT explain_memoize('
SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
- explain_memoize
--------------------------------------------------------------------------------
+ explain_memoize
+----------------------------------------------------------------------------------
Nested Loop (actual rows=4 loops=N)
-> Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
Heap Fetches: N
- -> Memoize (actual rows=2 loops=N)
+ -> Memoize (actual rows=2.00 loops=N)
Cache Key: f1.f
Cache Mode: binary
Hits: 0 Misses: 2 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
+ -> Index Only Scan using flt_f_idx on flt f2 (actual rows=2.00 loops=N)
Index Cond: (f <= f1.f)
Heap Fetches: N
(10 rows)
@@ -300,32 +300,32 @@ ANALYZE strtest;
-- Ensure we get 3 hits and 3 misses
SELECT explain_memoize('
SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
- explain_memoize
-----------------------------------------------------------------------------------
+ explain_memoize
+-------------------------------------------------------------------------------------
Nested Loop (actual rows=24 loops=N)
-> Seq Scan on strtest s1 (actual rows=6 loops=N)
Disabled: true
- -> Memoize (actual rows=4 loops=N)
+ -> Memoize (actual rows=4.00 loops=N)
Cache Key: s1.n
Cache Mode: binary
Hits: 3 Misses: 3 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Scan using strtest_n_idx on strtest s2 (actual rows=4 loops=N)
+ -> Index Scan using strtest_n_idx on strtest s2 (actual rows=4.00 loops=N)
Index Cond: (n <= s1.n)
(9 rows)
-- Ensure we get 3 hits and 3 misses
SELECT explain_memoize('
SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
- explain_memoize
-----------------------------------------------------------------------------------
+ explain_memoize
+-------------------------------------------------------------------------------------
Nested Loop (actual rows=24 loops=N)
-> Seq Scan on strtest s1 (actual rows=6 loops=N)
Disabled: true
- -> Memoize (actual rows=4 loops=N)
+ -> Memoize (actual rows=4.00 loops=N)
Cache Key: s1.t
Cache Mode: binary
Hits: 3 Misses: 3 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Scan using strtest_t_idx on strtest s2 (actual rows=4 loops=N)
+ -> Index Scan using strtest_t_idx on strtest s2 (actual rows=4.00 loops=N)
Index Cond: (t <= s1.t)
(9 rows)
@@ -348,7 +348,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
-> Nested Loop (actual rows=16 loops=N)
-> Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
Heap Fetches: N
- -> Memoize (actual rows=4 loops=N)
+ -> Memoize (actual rows=4.00 loops=N)
Cache Key: t1_1.a
Cache Mode: logical
Hits: 3 Misses: 1 Evictions: Zero Overflows: 0 Memory Usage: NkB
@@ -358,7 +358,7 @@ SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
-> Nested Loop (actual rows=16 loops=N)
-> Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
Heap Fetches: N
- -> Memoize (actual rows=4 loops=N)
+ -> Memoize (actual rows=4.00 loops=N)
Cache Key: t1_2.a
Cache Mode: logical
Hits: 3 Misses: 1 Evictions: Zero Overflows: 0 Memory Usage: NkB
@@ -378,7 +378,7 @@ ON t1.a = t2.a;', false);
Nested Loop (actual rows=16 loops=N)
-> Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
Heap Fetches: N
- -> Memoize (actual rows=4 loops=N)
+ -> Memoize (actual rows=4.00 loops=N)
Cache Key: t1.a
Cache Mode: logical
Hits: 3 Misses: 1 Evictions: Zero Overflows: 0 Memory Usage: NkB
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 6f80b62a3b..1dbe6ff54f 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2367,7 +2367,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
@@ -2940,7 +2940,7 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);');
-> Seq Scan on ab_a1_b1 ab_a1_1 (actual rows=1 loops=1)
-> Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
-> Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=1 loops=1)
- -> Materialize (actual rows=1 loops=3)
+ -> Materialize (actual rows=1.00 loops=3)
Storage: Memory Maximum Storage: NkB
-> Append (actual rows=1 loops=1)
-> Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
@@ -2983,12 +2983,12 @@ set enable_hashjoin = off;
set enable_mergejoin = off;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=6 loops=1)
-> Seq Scan on tbl1 (actual rows=2 loops=1)
- -> Append (actual rows=3 loops=2)
- -> Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=2)
+ -> Append (actual rows=3.00 loops=2)
+ -> Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=2)
Index Cond: (col1 < tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
Index Cond: (col1 < tbl1.col1)
@@ -3004,14 +3004,14 @@ select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=2 loops=1)
-> Seq Scan on tbl1 (actual rows=2 loops=1)
- -> Append (actual rows=1 loops=2)
+ -> Append (actual rows=1.00 loops=2)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
+ -> Index Scan using tprt2_idx on tprt_2 (actual rows=1.00 loops=2)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt3_idx on tprt_3 (never executed)
Index Cond: (col1 = tbl1.col1)
@@ -3049,16 +3049,16 @@ order by tbl1.col1, tprt.col1;
insert into tbl1 values (1001), (1010), (1011);
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=23 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=5 loops=5)
- -> Index Scan using tprt1_idx on tprt_1 (actual rows=2 loops=5)
+ -> Append (actual rows=4.60 loops=5)
+ -> Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=5)
Index Cond: (col1 < tbl1.col1)
- -> Index Scan using tprt2_idx on tprt_2 (actual rows=3 loops=4)
+ -> Index Scan using tprt2_idx on tprt_2 (actual rows=2.75 loops=4)
Index Cond: (col1 < tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=1 loops=2)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=1.00 loops=2)
Index Cond: (col1 < tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 < tbl1.col1)
@@ -3070,16 +3070,16 @@ select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Nested Loop (actual rows=3 loops=1)
-> Seq Scan on tbl1 (actual rows=5 loops=1)
- -> Append (actual rows=1 loops=5)
+ -> Append (actual rows=0.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt2_idx on tprt_2 (actual rows=1 loops=2)
+ -> Index Scan using tprt2_idx on tprt_2 (actual rows=1.00 loops=2)
Index Cond: (col1 = tbl1.col1)
- -> Index Scan using tprt3_idx on tprt_3 (actual rows=0 loops=3)
+ -> Index Scan using tprt3_idx on tprt_3 (actual rows=0.33 loops=3)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt4_idx on tprt_4 (never executed)
Index Cond: (col1 = tbl1.col1)
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index a809036453..4732ad6bfb 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -583,17 +583,17 @@ alter table tenk2 set (parallel_workers = 0);
explain (analyze, timing off, summary off, costs off, buffers off)
select count(*) from tenk1, tenk2 where tenk1.hundred > 1
and tenk2.thousand=0;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------
Aggregate (actual rows=1 loops=1)
-> Nested Loop (actual rows=98000 loops=1)
-> Seq Scan on tenk2 (actual rows=10 loops=1)
Filter: (thousand = 0)
Rows Removed by Filter: 9990
- -> Gather (actual rows=9800 loops=10)
+ -> Gather (actual rows=9800.00 loops=10)
Workers Planned: 4
Workers Launched: 4
- -> Parallel Seq Scan on tenk1 (actual rows=1960 loops=50)
+ -> Parallel Seq Scan on tenk1 (actual rows=1960.00 loops=50)
Filter: (hundred > 1)
Rows Removed by Filter: 40
(11 rows)
@@ -617,21 +617,21 @@ begin
end;
$$;
select * from explain_parallel_sort_stats();
- explain_parallel_sort_stats
---------------------------------------------------------------------------
+ explain_parallel_sort_stats
+-----------------------------------------------------------------------------
Nested Loop Left Join (actual rows=30000 loops=1)
-> Values Scan on "*VALUES*" (actual rows=3 loops=1)
- -> Gather Merge (actual rows=10000 loops=3)
+ -> Gather Merge (actual rows=10000.00 loops=3)
Workers Planned: 4
Workers Launched: 4
- -> Sort (actual rows=2000 loops=15)
+ -> Sort (actual rows=2000.00 loops=15)
Sort Key: tenk1.ten
Sort Method: quicksort Memory: xxx
Worker 0: Sort Method: quicksort Memory: xxx
Worker 1: Sort Method: quicksort Memory: xxx
Worker 2: Sort Method: quicksort Memory: xxx
Worker 3: Sort Method: quicksort Memory: xxx
- -> Parallel Seq Scan on tenk1 (actual rows=2000 loops=15)
+ -> Parallel Seq Scan on tenk1 (actual rows=2000.00 loops=15)
Filter: (ten < 100)
(14 rows)
@@ -1170,12 +1170,12 @@ explain (costs off)
SAVEPOINT settings;
SET LOCAL debug_parallel_query = 1;
EXPLAIN (analyze, timing off, summary off, costs off, buffers off) SELECT * FROM tenk1;
- QUERY PLAN
--------------------------------------------------------------
+ QUERY PLAN
+----------------------------------------------------------------
Gather (actual rows=10000 loops=1)
Workers Planned: 4
Workers Launched: 4
- -> Parallel Seq Scan on tenk1 (actual rows=2000 loops=5)
+ -> Parallel Seq Scan on tenk1 (actual rows=2000.00 loops=5)
(4 rows)
ROLLBACK TO SAVEPOINT settings;
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 86621dcec0..6aad02156c 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -586,7 +586,7 @@ begin
$1)
loop
ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
- ln := regexp_replace(ln, 'actual rows=\d+ loops=\d+', 'actual rows=N loops=N');
+ ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
return next ln;
end loop;
--
2.34.1
On 17/2/2025 15:19, Robert Haas wrote:
On Mon, Feb 17, 2025 at 3:08 AM Ilia Evdokimov
if (nloops > 1)Instead of:
if (nloops > 1 && rows_is_fractonal)
I don't think it's really safe to just cast a double back to int64. In
practice, the number of tuples should never be large enough to
overflow int64, but if it did, this result would be nonsense. Also, if
the double ever lost precision, the result would be nonsense. If we
want to have an exact count of tuples, we ought to change ntuples and
ntuples2 to be uint64. But I don't think we should do that in this
patch, because that adds a whole bunch of new problems to worry about
and might cause us to get nothing committed. Instead, I think we
should just always show two decimal digits if there's more than one
loop.That's simpler than what the patch currently does and avoids this
problem. Perhaps it's objectionable for some other reason, but if so,
can somebody please spell out what that reason is so we can talk about
it?
I can understand two decimal places. You might be concerned about
potential issues with some codes that parse PostgreSQL explains.
However, I believe it would be beneficial to display fractional parts
only when iterations yield different numbers of tuples. Given that I
often work with enormous explains, I think this approach would enhance
the readability and comprehension of the output. Frequently, I may see
only part of the EXPLAIN on the screen. A floating-point row number
format may immediately give an idea about parameterisation (or another
reason for the subtree's variability) and trace it down to the source.
--
regards, Andrei Lepikhov
On 18.02.2025 23:55, Andrei Lepikhov wrote:
On 17/2/2025 15:19, Robert Haas wrote:
On Mon, Feb 17, 2025 at 3:08 AM Ilia Evdokimov
if (nloops > 1)Instead of:
if (nloops > 1 && rows_is_fractonal)
I don't think it's really safe to just cast a double back to int64. In
practice, the number of tuples should never be large enough to
overflow int64, but if it did, this result would be nonsense. Also, if
the double ever lost precision, the result would be nonsense. If we
want to have an exact count of tuples, we ought to change ntuples and
ntuples2 to be uint64. But I don't think we should do that in this
patch, because that adds a whole bunch of new problems to worry about
and might cause us to get nothing committed. Instead, I think we
should just always show two decimal digits if there's more than one
loop.That's simpler than what the patch currently does and avoids this
problem. Perhaps it's objectionable for some other reason, but if so,
can somebody please spell out what that reason is so we can talk about
it?I can understand two decimal places. You might be concerned about
potential issues with some codes that parse PostgreSQL explains.
However, I believe it would be beneficial to display fractional parts
only when iterations yield different numbers of tuples. Given that I
often work with enormous explains, I think this approach would enhance
the readability and comprehension of the output. Frequently, I may see
only part of the EXPLAIN on the screen. A floating-point row number
format may immediately give an idea about parameterisation (or another
reason for the subtree's variability) and trace it down to the source.
The idea of indicating to the user that different iterations produced
varying numbers of rows is quite reasonable. Most likely, this would
require adding a new boolean field to the Instrumentation structure,
which would track this information by comparing the rows value from the
current and previous iterations.
However, there is a major issue: this case would be quite difficult to
document clearly. Even with an example and explanatory text, users may
still be confused about why rows=100 means the same number of rows on
all iterations, while rows=100.00 indicates variation. Even if we
describe this in the documentation, a user seeing rows=100.00 will most
likely assume it represents an average of 100 rows per iteration and may
still not realize that the actual number of rows varied.
If we want to convey this information more clearly, we should consider a
more explicit approach. For example, instead of using a fractional
value, we could display the minimum and maximum row counts observed
during execution (e.g.,rows=10..20, formatting details could be
discussed). However, in my opinion, this discussion is beyond the scope
of this thread.
Any thoughts?
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
On 19/2/2025 15:01, Ilia Evdokimov wrote:
If we want to convey this information more clearly, we should consider a
more explicit approach. For example, instead of using a fractional
value, we could display the minimum and maximum row counts observed
during execution (e.g.,rows=10..20, formatting details could be
discussed). However, in my opinion, this discussion is beyond the scope
of this thread.
I think this approach is redundant. Usually, a problem is an unknown
number of tuples because, having millions of loops, we produce a small
number of tuples, but each tuple costs a lot because of a subplan or
something like that. By printing fractional digits (of course, we need
to view significant ones, but we already agreed on a couple of digits
for now), we may resolve this corner-case problem fully - at least all
examples in my experience would be resolved, and I have even maintained
a patch for years to manage this.
Moreover, it is too simple to implement. I just can't imagine why not to
do it right now.
--
regards, Andrei Lepikhov
On Wed, Feb 19, 2025 at 9:01 AM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:
The idea of indicating to the user that different iterations produced
varying numbers of rows is quite reasonable. Most likely, this would
require adding a new boolean field to the Instrumentation structure,
which would track this information by comparing the rows value from the
current and previous iterations.
Yep.
However, there is a major issue: this case would be quite difficult to
document clearly. Even with an example and explanatory text, users may
still be confused about why rows=100 means the same number of rows on
all iterations, while rows=100.00 indicates variation. Even if we
describe this in the documentation, a user seeing rows=100.00 will most
likely assume it represents an average of 100 rows per iteration and may
still not realize that the actual number of rows varied.
I imagine this is a surmountable problem.
If we want to convey this information more clearly, we should consider a
more explicit approach. For example, instead of using a fractional
value, we could display the minimum and maximum row counts observed
during execution (e.g.,rows=10..20, formatting details could be
discussed). However, in my opinion, this discussion is beyond the scope
of this thread.
I quite agree. I think we've spent too much time on this already; this
idea was first proposed in 2009, and we just haven't gotten around to
doing anything about it. Redesigning the feature a few more times
(with an expanded scope) isn't going to make that better.
So, I've committed v11-0001. I'm not altogether convinced that
v11-0002 is necessary -- no portion of the documentation that it
modifies is falsified by the committed patch. Maybe we can just call
this one done for now and move on?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Feb 21, 2025 at 4:20 PM Robert Haas <robertmhaas@gmail.com> wrote:
So, I've committed v11-0001. I'm not altogether convinced that
v11-0002 is necessary -- no portion of the documentation that it
modifies is falsified by the committed patch. Maybe we can just call
this one done for now and move on?
Well, not quite yet, at least. There are two buildfarm failures since
I committed this. The first is here:
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=conchuela&dt=2025-02-21%2021%3A22%3A30
diff -U3 /home/pgbf/buildroot/HEAD/pgsql.build/src/test/regress/expected/explain.out
/home/pgbf/buildroot/HEAD/pgsql.build/src/test/regress/results/explain.out
--- /home/pgbf/buildroot/HEAD/pgsql.build/src/test/regress/expected/explain.out
2025-02-21 22:20:39.229225000 +0100
+++ /home/pgbf/buildroot/HEAD/pgsql.build/src/test/regress/results/explain.out
2025-02-21 22:22:38.193054000 +0100
@@ -528,7 +528,7 @@
"Plan Rows": 0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
- "Actual Rows": 0.0, +
+ "Actual Rows": 0, +
"Actual Loops": 0, +
"Startup Cost": 0.0, +
"Async Capable": false, +
@@ -575,7 +575,7 @@
"Plan Rows": 0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
- "Actual Rows": 0.0, +
+ "Actual Rows": 0, +
"Actual Loops": 0, +
"Startup Cost": 0.0, +
"Async Capable": false, +
I imagine what happened here is that the parallel workers didn't
start, and so nloops was 1 instead of >1, and so we got an integer
output instead of a fractional one. It looks like there's some kind of
JSON-based EXPLAIN filtering happening here, so maybe we should just
also be filtering out the actual rows column? But I fear this will
turn out to be a problem for other parallel-query using tests that
were modified by this commit: it can always happen that the workers
start slowly enough that the leader finishes the whole query before
the worker starts up. Maybe making the number of decimal digits
contingent on nloops wasn't such a hot idea - I think it means that
every parallel query has a small but non-zero probability of producing
different EXPLAIN output in the probably-rare case where the worker
startup is slow.
The other failure is here:
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=loach&dt=2025-02-21%2021%3A25%3A19
This failure seems not to have uploaded any log artifacts, so I'm not
sure how to tell what caused this.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
Well, not quite yet, at least. There are two buildfarm failures since
I committed this. The first is here:
A very significant fraction of the buildfarm is now pink.
If you don't have a fix pretty nearly ready, please revert.
I imagine what happened here is that the parallel workers didn't
start, and so nloops was 1 instead of >1, and so we got an integer
output instead of a fractional one.
Sounds plausible. I note that this run:
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=skimmer&dt=2025-02-21%2023%3A00%3A10
got through a couple of iterations of the core tests before failing,
showing that it's not 100% reproducible even on the machines where
it's happening.
regards, tom lane
On Fri, Feb 21, 2025 at 7:04 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
A very significant fraction of the buildfarm is now pink.
If you don't have a fix pretty nearly ready, please revert.
When we're going to do a release, you want no commits for at least 24
hours before the release so that we can make sure the buildfarm is
clean. But when I commit something and the buildfarm fails, you want
it reverted within a handful of hours before I can even be sure of
having seen all the failures the commit caused, let alone had time to
think about what the best fix might be. It doesn't make sense to me
that we need 24 hours to be sure that the buildfarm is passing, but in
3 hours I'm supposed to see all the failures -- including the ones
that only happen on buildfarm animals that run once a day, I guess? --
and analyze them -- and decide on a fix -- naturally without any sort
of discussion because there's no time for that -- and code the fix --
and push it. I have a really hard time seeing how that's a reasonable
expectation.
I understand that the buildfarm can't be red all the time or nobody
can distinguish the problems they caused from preexisting ones. At the
same time, it seems completely unreasonable for us to say that, on the
one hand, the buildfarm has to be green at absolutely all times, and
on the other time, buildfarm owners are not required to provide any
sort of resources to test things before they are committed. IMHO, one
of those policies absolutely has to change. The current situation is
way too stressful for committers and it's burning people out and
making them unwilling to commit things -- or if they do commit things,
then they end up insta-reverting them, committing them again later,
maybe insta-reverting them a second time because they didn't actually
find all the problems the first time, and then maybe even round three,
four, or five. The commit log ends up with a bunch of garbage from the
repeated commits and reverts, and if it goes on long enough,
eventually somebody shows up to say "wow, this patch seems to be in
terrible shape, maybe it shouldn't ever be committed again" right when
the committer's stress level is already going through the ceiling. And
sometimes that is justified, but sometimes it isn't.
In the case of my commit today, the failures are the result of a
2-line regression diff with no functional impact that neither CI nor
any of the 11 reviewers noticed. That just shouldn't be the sort of
thing that results in somebody having to work evenings and weekends.
Perhaps if it DIDN'T result in a committer having to work evenings and
weekends, it wouldn't have taken 16 years for us to do something about
that problem.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Feb 21, 2025 at 7:04 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Sounds plausible. I note that this run:
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=skimmer&dt=2025-02-21%2023%3A00%3A10
got through a couple of iterations of the core tests before failing,
showing that it's not 100% reproducible even on the machines where
it's happening.
Yeah, I pushed an attempt at a band-aid fix. Hopefully that will make
it clear whether there are other problems and how bad they are. It
looks like the XversionUpgrade tests are not this commit's fault.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
Yeah, I pushed an attempt at a band-aid fix. Hopefully that will make
it clear whether there are other problems and how bad they are. It
looks like the XversionUpgrade tests are not this commit's fault.
They are not, they're Jeff's problem.
I apologize if I ruffled your feathers. But the buildfarm is a shared
resource and it doesn't do to leave it significantly broken over a
weekend, which I feared you might be about to do. Committing stuff
at 4PM on a Friday is perhaps not the best plan if your intention
is to not work weekends.
On a positive note, at least some of the formerly-pink members
seem to be happy now. So as you hoped, we can get more info about
whether there are less-frequent problems.
As for a permanent fix: maybe, instead of printing fractions when
actual nloops > 1, we should print fractions if *potential*
nloops > 1? That is, anytime we're on the inside of a nestloop,
under a Gather, etc. Seems like this'd not be too hard to track
while recursively descending the PlanState tree.
regards, tom lane
Hi,
On 2025-02-21 20:09:24 -0500, Robert Haas wrote:
In the case of my commit today, the failures are the result of a
2-line regression diff with no functional impact that neither CI nor
any of the 11 reviewers noticed. That just shouldn't be the sort of
thing that results in somebody having to work evenings and weekends.
Perhaps if it DIDN'T result in a committer having to work evenings and
weekends, it wouldn't have taken 16 years for us to do something about
that problem.
I think you're right that it really depends on what the breakage is.
If just about all animals die early in the test, it's more important the
problem gets fixed more or the faulty commit(s) reverted. For one it prevents
detecting other problems, as later, independent failures are hidden. For
another it quite likely will affect other developers "at home", too.
I'd also say that breaking CI and BF is probably something I'd consider more
urgent, as that could indicate the commit was just generally less well tested.
If, as the case here, a few animals fail with a minor regression.diffs output,
one that doesn't indicate any major issue but just some corner case change in
output, it's ok to leave it for the next day or so (*).
Greetings,
Andres Freund
(*) With some adjustments based on circumstances. If the breakage is over a
weekend, it's perhaps a bit less urgent. However, if it's the Friday before a
feature freeze, it's obviously a differnt story.
Hi,
On 2025-02-21 21:11:07 -0500, Tom Lane wrote:
Robert Haas <robertmhaas@gmail.com> writes:
Yeah, I pushed an attempt at a band-aid fix. Hopefully that will make
it clear whether there are other problems and how bad they are. It
looks like the XversionUpgrade tests are not this commit's fault.They are not, they're Jeff's problem.
I apologize if I ruffled your feathers. But the buildfarm is a shared
resource and it doesn't do to leave it significantly broken over a
weekend, which I feared you might be about to do. Committing stuff
at 4PM on a Friday is perhaps not the best plan if your intention
is to not work weekends.
IDK, there were a 8 animal showing rather minor and easy to identify
regression.diffs changes, with 2-3 as many animals being fine. Of course it's
not desirable to have to dig through such failures when checking whether
another patch caused problems, but it's also not the end of the world if
things stay that way for a bit, particularly on the weekend.
Greetings,
Andres Freund
On Fri, Feb 21, 2025 at 9:11 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I apologize if I ruffled your feathers. But the buildfarm is a shared
resource and it doesn't do to leave it significantly broken over a
weekend, which I feared you might be about to do. Committing stuff
at 4PM on a Friday is perhaps not the best plan if your intention
is to not work weekends.
I really, really think we need some policy changes. Several committers
have told me that they literally won't commit anything on Friday for
fear of this exact kind of problem, but that means we lose a lot of
hours every week that people could potentially be dealing with patches
that have been waiting for an exceedingly long time for someone to
commit them. I feel like this is entirely a self-inflicted injury. We
could decide that CI is primary and the buildfarm is secondary, so
that as long as CI is green, you have a week to deal with anything
else that breaks. We could decide, as I said on the new thread I
started, to insist on having a vehicle for pre-commit buildfarm
testing. We could decide to just chill out over a couple of lines of
regression test diffs over a weekend where most people aren't going to
be working anyway. We could *at the very least* decide that you get 24
hours before you have to revert, perhaps with an exception if we're in
the last 14 days before feature freeze. There are probably other
things we could do. Yelling at people every time a patch turns the
buildfarm red for a few hours seems worse to me than MANY of the other
options.
Really, I feel like the way the buildfarm works is a relic of the
past. We're living in an era where cfbot is testing hundreds of
branches on a daily basis, all of which are completely untrusted code
downloaded from the Internet, but our buildfarm can't manage more than
about six and can't provide any testing resources even to committers.
In 2025, that's really far from the state of the art -- and CI seems
to be progressing a great deal faster than the BF, so the gap between
them figures to only get wider.
On a positive note, at least some of the formerly-pink members
seem to be happy now. So as you hoped, we can get more info about
whether there are less-frequent problems.
Cool.
As for a permanent fix: maybe, instead of printing fractions when
actual nloops > 1, we should print fractions if *potential*
nloops > 1? That is, anytime we're on the inside of a nestloop,
under a Gather, etc. Seems like this'd not be too hard to track
while recursively descending the PlanState tree.
I guess we could do that, but how about just always displaying two
decimal places? I feel like you're really worried that people will be
confused if the row count is not an integer, but I'm not sure anyone
else has echoed that concern, and I am doubtful that it's really a
problem. Either people don't understand that the row count is divided
through by nloops, in which case the decimal places might actually
give them a clue that they've misunderstood, or they do understand
that, in which case the decimal places are not confusing. I feel like
we've gone to a lot of trouble to come up with complex solutions --
what was actually committed was one of the simpler things proposed --
but I am still not convinced that the simplest solution isn't for the
best.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Feb 21, 2025 at 9:14 PM Andres Freund <andres@anarazel.de> wrote:
I'd also say that breaking CI and BF is probably something I'd consider more
urgent, as that could indicate the commit was just generally less well tested.
This is possible, but I think we oversell it by quite a lot. It's
quite possible for someone to put hundreds of hours into a patch and
forget to check CI before pushing; and it's also quite possible for
someone to do a terrible job vetting a patch where CI is clean. When
somebody is already stressed out about their patch breaking the
buildfarm, the very last thing they need is somebody who hasn't read
the patch or understood what the problems are to show up and say "hey,
maybe this patch should be reverted for all time and never considered
again ever!". It's just making a very stressful situation more
stressful. And it's cheap. If somebody shows up and says "hey, this
was improvidently committed for the following six design-level
reasons," that is abundantly fair and deserves major respect. Such
reviews take real time, thought, and work. Idly speculating that
someone's failure to check CI is a sign that they've also done
everything else wrong takes almost no work at all. I hate that we do
that to people. As much as I hate it when it happens to me, I think I
hate it even more when it happens to other people. It's a terrible way
to treat people who have poured their heart and soul into becoming
committers and who really care about the project, at least until we
beat the caring out of them.
But the real point of my previous email is that I just do not think
it's reasonable to expect people to fix complex programming problems
within hours. As much as I can be grumpy about CI, anything that goes
wrong with CI should in theory be something you can avoid ever having
to deal with on a Friday night no matter when you choose to commit,
because you can test things in advance. I know the BF has more
configurations than CI, but instead of having LESS capability to test
things in advance, it has NONE. Twenty years ago, post-commit testing
was probably the best you could hope for, but today it isn't.
--
Robert Haas
EDB: http://www.enterprisedb.com
This was very painful to read. +1 on making changes. Both for a
culture change to require less urgency over the weekend if it's a
minor failure, and probably also a tooling change to make this less of
a problem in general.
On Sat, 22 Feb 2025 at 04:38, Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Feb 21, 2025 at 9:14 PM Andres Freund <andres@anarazel.de> wrote:
I'd also say that breaking CI and BF is probably something I'd consider more
urgent, as that could indicate the commit was just generally less well tested.This is possible, but I think we oversell it by quite a lot. It's
quite possible for someone to put hundreds of hours into a patch and
forget to check CI before pushing;
...snip...
Twenty years ago, post-commit testing
was probably the best you could hope for, but today it isn't.
100% agreed. As someone also working on other open source projects,
this reads incredibly foreign. This is a problem that simply cannot
happen in most projects I've worked on: GitHub will simply not allow
you to push to master/main if CI fails (at least not without an
explicit override). Postgres is the only project I work on where all
these things only run post-push to master.
To be clear, I'm not saying that we should be using GitHub for this,
although I personally think that would be a good idea given our CI is
triggered by pushes to GitHub. But it at least seems like one thing
that we need is a process change to make it less likely that people
accidentally push broken changes to master. e.g. We create a staging
repo (which could be on GitHub) and each committer has their own
branch (or branch prefix). Then when a committer pushes there, CI is
run and probably also some significant part of the build farm (and
maybe a committer could even opt in to running the full build farm).
If they pass, then the branch is automatically rebased on top of
master and pushed to the production repo its master branch.
On 22.02.2025 00:20, Robert Haas wrote:
So, I've committed v11-0001. I'm not altogether convinced that
v11-0002 is necessary -- no portion of the documentation that it
modifies is falsified by the committed patch. Maybe we can just call
this one done for now and move on?
Not quite. If no one objects to leaving the documentation as it is,
there is one nuance that needs to be fixed—in example [0], there is a
node with loops=10, so it should be updated to reflect the current
state. I've attached a patch for that.
Attachments:
v12-0001-Update-documentation-after-changing-the-rows-format.patchtext/x-patch; charset=UTF-8; name=v12-0001-Update-documentation-after-changing-the-rows-format.patchDownload
From c17c1926598fbb35a829a98ea35db3c41f1c7ccc Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Date: Mon, 24 Feb 2025 13:55:15 +0300
Subject: [PATCH v12] Update documentation after changing the rows format in
EXPLAIN ANALYZE
Following commit ddb17e3, the format of the average rows" field
in EXPLAIN ANALYZE was modified to always display fractional values
when loops > 1. This commit updates the example in the documentation
to reflect the new output format, ensuring consistency with the latest behavior.
---
doc/src/sgml/perform.sgml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index a502a2aaba..6a3dcc0bfc 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -730,7 +730,7 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
Index Cond: (unique1 < 10)
Buffers: shared hit=2
- -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
+ -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1.00 loops=10)
Index Cond: (unique2 = t1.unique2)
Buffers: shared hit=24 read=6
Planning:
--
2.34.1
Hi,
Regarding the patch push, I am not a committer, but perhaps my
perspective might be interesting. When I noticed late on Friday evening
that the tests had failed, I was quite anxious about the situation,
thinking I would need to fix it right away. However, Robert committed
the fix before that.
In general, when someone commits in any project, I believe the scale of
the commit should be taken into account. In the case of postgres, if the
commit changes code in critical areas like the planner, WAL, or some
API, or involves large code changes, it’s important to be prepared to
fix any issues that may arise. However, when changes are more minor—such
as documentation, renaming something, or small refactors—committing late
on a Friday may be less of a concern.
In other words, the larger the changes or the more vulnerable the areas
of the code, the more one should be prepared for potential issues. But
how to determine this boundary in postgres, I am not sure. I am
confident that you will have a much better sense and experience of how
to handle it.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
On Fri, Feb 21, 2025 at 10:00 PM Robert Haas <robertmhaas@gmail.com> wrote:
I guess we could do that, but how about just always displaying two
decimal places? I feel like you're really worried that people will be
confused if the row count is not an integer, but I'm not sure anyone
else has echoed that concern, and I am doubtful that it's really a
problem. Either people don't understand that the row count is divided
through by nloops, in which case the decimal places might actually
give them a clue that they've misunderstood, or they do understand
that, in which case the decimal places are not confusing. I feel like
we've gone to a lot of trouble to come up with complex solutions --
what was actually committed was one of the simpler things proposed --
but I am still not convinced that the simplest solution isn't for the
best.
So here's a patch for that. Thoughts?
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
0001-EXPLAIN-Always-use-two-fractional-digits-for-row-cou.patchapplication/octet-stream; name=0001-EXPLAIN-Always-use-two-fractional-digits-for-row-cou.patchDownload
From 7fd8280b72bc0f252c7a0718f241bddbef65ec68 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 24 Feb 2025 10:57:11 -0500
Subject: [PATCH] EXPLAIN: Always use two fractional digits for row counts.
Commit ddb17e387aa28d61521227377b00f997756b8a27 attempted to avoid
confusing users by displaying digits after the decimal point only when
nloops > 1, since it's impossible to have a fraction row count after a
single iteration. However, this made the regression tests unstable since
parallal queries will have nloops>1 for all nodes below the Gather or
Gather Merge in normal cases, but if the workers don't start in time and
the leader finishes all the work, they will suddenly have nloops==1,
making it unpredictable whether the digits after the decimal point would
be displayed or not.
Various fixes are possible here. For example, it has previously been
proposed that we should try to display the digits after the decimal
point only if rows/nloops is an integer, but rows is a float so it's not
theoretically an exact quantity -- precision could be lost in extreme
cases. It has also been proposed that we should try to display the
digits after the decimal point only if we're under some sort of
construct that could potentially cause looping regardless of whether it
actually does. While such ideas are not without merit, this patch adopts
the much simpler solution of always display two decimal digits. If that
approach stands up to scrutiny from the buildfarm and human users, it
spares us the trouble of doing anything more complex; if not, we can
reassess.
This commit incidentally reverts 44cbba9a7f51a3888d5087fc94b23614ba2b81f2,
which should no longer be needed.
---
.../expected/level_tracking.out | 24 +-
.../postgres_fdw/expected/postgres_fdw.out | 60 +--
src/backend/commands/explain.c | 34 +-
src/test/regress/expected/brin_multi.out | 72 +--
src/test/regress/expected/explain.out | 89 ++--
src/test/regress/expected/gin.out | 20 +-
.../regress/expected/incremental_sort.out | 20 +-
src/test/regress/expected/matview.out | 8 +-
src/test/regress/expected/memoize.out | 94 ++--
src/test/regress/expected/merge.out | 124 ++---
src/test/regress/expected/misc_functions.out | 60 +--
src/test/regress/expected/partition_prune.out | 480 +++++++++---------
src/test/regress/expected/select.out | 6 +-
src/test/regress/expected/select_into.out | 16 +-
src/test/regress/expected/select_parallel.out | 12 +-
src/test/regress/expected/subselect.out | 12 +-
src/test/regress/expected/tidscan.out | 16 +-
src/test/regress/sql/explain.sql | 3 -
18 files changed, 562 insertions(+), 588 deletions(-)
diff --git a/contrib/pg_stat_statements/expected/level_tracking.out b/contrib/pg_stat_statements/expected/level_tracking.out
index 297ebc5159e..03bea14d5da 100644
--- a/contrib/pg_stat_statements/expected/level_tracking.out
+++ b/contrib/pg_stat_statements/expected/level_tracking.out
@@ -904,16 +904,16 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t;
(1 row)
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT 100;
- QUERY PLAN
---------------------------------
- Result (actual rows=1 loops=1)
+ QUERY PLAN
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
(1 row)
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
DECLARE foocur CURSOR FOR SELECT * FROM stats_track_tab;
- QUERY PLAN
------------------------------------------------------
- Seq Scan on stats_track_tab (actual rows=0 loops=1)
+ QUERY PLAN
+--------------------------------------------------------
+ Seq Scan on stats_track_tab (actual rows=0.00 loops=1)
(1 row)
SELECT toplevel, calls, query FROM pg_stat_statements
@@ -937,16 +937,16 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t;
(1 row)
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT 100;
- QUERY PLAN
---------------------------------
- Result (actual rows=1 loops=1)
+ QUERY PLAN
+-----------------------------------
+ Result (actual rows=1.00 loops=1)
(1 row)
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
DECLARE foocur CURSOR FOR SELECT * FROM stats_track_tab;
- QUERY PLAN
------------------------------------------------------
- Seq Scan on stats_track_tab (actual rows=0 loops=1)
+ QUERY PLAN
+--------------------------------------------------------
+ Seq Scan on stats_track_tab (actual rows=0.00 loops=1)
(1 row)
SELECT toplevel, calls, query FROM pg_stat_statements
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index daa3b1d7a6d..8447b289cb7 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -11670,15 +11670,15 @@ SELECT * FROM local_tbl, async_pt WHERE local_tbl.a = async_pt.a AND local_tbl.c
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
SELECT * FROM local_tbl, async_pt WHERE local_tbl.a = async_pt.a AND local_tbl.c = 'bar';
- QUERY PLAN
--------------------------------------------------------------------------------
- Nested Loop (actual rows=1 loops=1)
- -> Seq Scan on local_tbl (actual rows=1 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------------
+ Nested Loop (actual rows=1.00 loops=1)
+ -> Seq Scan on local_tbl (actual rows=1.00 loops=1)
Filter: (c = 'bar'::text)
Rows Removed by Filter: 1
- -> Append (actual rows=1 loops=1)
+ -> Append (actual rows=1.00 loops=1)
-> Async Foreign Scan on async_p1 async_pt_1 (never executed)
- -> Async Foreign Scan on async_p2 async_pt_2 (actual rows=1 loops=1)
+ -> Async Foreign Scan on async_p2 async_pt_2 (actual rows=1.00 loops=1)
-> Seq Scan on async_p3 async_pt_3 (never executed)
Filter: (a = local_tbl.a)
(9 rows)
@@ -11916,20 +11916,20 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt WHERE a < 3000) FROM async_pt WHERE a < 3000) t2 ON t1.a = t2.a;
- QUERY PLAN
------------------------------------------------------------------------------------------
- Nested Loop Left Join (actual rows=1 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------------------------------
+ Nested Loop Left Join (actual rows=1.00 loops=1)
Join Filter: (t1.a = async_pt.a)
Rows Removed by Join Filter: 399
InitPlan 1
- -> Aggregate (actual rows=1 loops=1)
- -> Append (actual rows=400 loops=1)
- -> Async Foreign Scan on async_p1 async_pt_4 (actual rows=200 loops=1)
- -> Async Foreign Scan on async_p2 async_pt_5 (actual rows=200 loops=1)
- -> Seq Scan on local_tbl t1 (actual rows=1 loops=1)
- -> Append (actual rows=400 loops=1)
- -> Async Foreign Scan on async_p1 async_pt_1 (actual rows=200 loops=1)
- -> Async Foreign Scan on async_p2 async_pt_2 (actual rows=200 loops=1)
+ -> Aggregate (actual rows=1.00 loops=1)
+ -> Append (actual rows=400.00 loops=1)
+ -> Async Foreign Scan on async_p1 async_pt_4 (actual rows=200.00 loops=1)
+ -> Async Foreign Scan on async_p2 async_pt_5 (actual rows=200.00 loops=1)
+ -> Seq Scan on local_tbl t1 (actual rows=1.00 loops=1)
+ -> Append (actual rows=400.00 loops=1)
+ -> Async Foreign Scan on async_p1 async_pt_1 (actual rows=200.00 loops=1)
+ -> Async Foreign Scan on async_p2 async_pt_2 (actual rows=200.00 loops=1)
(12 rows)
SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt WHERE a < 3000) FROM async_pt WHERE a < 3000) t2 ON t1.a = t2.a;
@@ -11960,15 +11960,15 @@ SELECT * FROM async_pt t1 WHERE t1.b === 505 LIMIT 1;
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
SELECT * FROM async_pt t1 WHERE t1.b === 505 LIMIT 1;
- QUERY PLAN
--------------------------------------------------------------------------
- Limit (actual rows=1 loops=1)
- -> Append (actual rows=1 loops=1)
- -> Async Foreign Scan on async_p1 t1_1 (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------
+ Limit (actual rows=1.00 loops=1)
+ -> Append (actual rows=1.00 loops=1)
+ -> Async Foreign Scan on async_p1 t1_1 (actual rows=0.00 loops=1)
Filter: (b === 505)
- -> Async Foreign Scan on async_p2 t1_2 (actual rows=0 loops=1)
+ -> Async Foreign Scan on async_p2 t1_2 (actual rows=0.00 loops=1)
Filter: (b === 505)
- -> Seq Scan on async_p3 t1_3 (actual rows=1 loops=1)
+ -> Seq Scan on async_p3 t1_3 (actual rows=1.00 loops=1)
Filter: (b === 505)
Rows Removed by Filter: 101
(9 rows)
@@ -12120,12 +12120,12 @@ DELETE FROM async_p2;
DELETE FROM async_p3;
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
SELECT * FROM async_pt;
- QUERY PLAN
--------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
- -> Async Foreign Scan on async_p1 async_pt_1 (actual rows=0 loops=1)
- -> Async Foreign Scan on async_p2 async_pt_2 (actual rows=0 loops=1)
- -> Seq Scan on async_p3 async_pt_3 (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
+ -> Async Foreign Scan on async_p1 async_pt_1 (actual rows=0.00 loops=1)
+ -> Async Foreign Scan on async_p2 async_pt_2 (actual rows=0.00 loops=1)
+ -> Seq Scan on async_p3 async_pt_3 (actual rows=0.00 loops=1)
(4 rows)
-- Clean up
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c0d614866a9..4271dd48e4e 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1998,10 +1998,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->timing)
appendStringInfo(es->str, "time=%.3f..%.3f ", startup_ms, total_ms);
- if (nloops > 1)
- appendStringInfo(es->str, "rows=%.2f loops=%.0f)", rows, nloops);
- else
- appendStringInfo(es->str, "rows=%.0f loops=%.0f)", rows, nloops);
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f)", rows, nloops);
}
else
{
@@ -2012,16 +2009,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
3, es);
}
- if (nloops > 1)
- {
- ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
- }
- else
- {
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
- }
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
}
}
else if (es->analyze)
@@ -2077,10 +2066,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
if (es->timing)
appendStringInfo(es->str, "time=%.3f..%.3f", startup_ms, total_ms);
- if (nloops > 1)
- appendStringInfo(es->str, "rows=%.2f loops=%.0f\n", rows, nloops);
- else
- appendStringInfo(es->str, "rows=%.0f loops=%.0f\n", rows, nloops);
+ appendStringInfo(es->str, "rows=%.2f loops=%.0f\n", rows, nloops);
}
else
{
@@ -2092,16 +2078,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
total_ms, 3, es);
}
- if (nloops > 1)
- {
- ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
- }
- else
- {
- ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
- ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
- }
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
}
ExplainCloseWorker(n, es);
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index f2d14658181..991b7eacada 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -847,11 +847,11 @@ SET enable_seqscan = off;
-- make sure the ranges were built correctly and 2023-01-01 eliminates all
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
- QUERY PLAN
--------------------------------------------------------------------------
- Bitmap Heap Scan on brin_date_test (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_date_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '2023-01-01'::date)
- -> Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '2023-01-01'::date)
(4 rows)
@@ -866,21 +866,21 @@ CREATE INDEX ON brin_timestamp_test USING brin (a timestamp_minmax_multi_ops) WI
SET enable_seqscan = off;
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_timestamp_test WHERE a = '2023-01-01'::timestamp;
- QUERY PLAN
-------------------------------------------------------------------------------
- Bitmap Heap Scan on brin_timestamp_test (actual rows=0 loops=1)
+ QUERY PLAN
+---------------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_timestamp_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
- -> Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '2023-01-01 00:00:00'::timestamp without time zone)
(4 rows)
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_timestamp_test WHERE a = '1900-01-01'::timestamp;
- QUERY PLAN
-------------------------------------------------------------------------------
- Bitmap Heap Scan on brin_timestamp_test (actual rows=0 loops=1)
+ QUERY PLAN
+---------------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_timestamp_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
- -> Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_timestamp_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '1900-01-01 00:00:00'::timestamp without time zone)
(4 rows)
@@ -894,21 +894,21 @@ CREATE INDEX ON brin_date_test USING brin (a date_minmax_multi_ops) WITH (pages_
SET enable_seqscan = off;
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_date_test WHERE a = '2023-01-01'::date;
- QUERY PLAN
--------------------------------------------------------------------------
- Bitmap Heap Scan on brin_date_test (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_date_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '2023-01-01'::date)
- -> Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '2023-01-01'::date)
(4 rows)
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_date_test WHERE a = '1900-01-01'::date;
- QUERY PLAN
--------------------------------------------------------------------------
- Bitmap Heap Scan on brin_date_test (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_date_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '1900-01-01'::date)
- -> Bitmap Index Scan on brin_date_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_date_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '1900-01-01'::date)
(4 rows)
@@ -923,21 +923,21 @@ CREATE INDEX ON brin_interval_test USING brin (a interval_minmax_multi_ops) WITH
SET enable_seqscan = off;
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
- QUERY PLAN
------------------------------------------------------------------------------
- Bitmap Heap Scan on brin_interval_test (actual rows=0 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_interval_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '@ 30 years ago'::interval)
- -> Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '@ 30 years ago'::interval)
(4 rows)
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
- QUERY PLAN
------------------------------------------------------------------------------
- Bitmap Heap Scan on brin_interval_test (actual rows=0 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_interval_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '@ 30 years'::interval)
- -> Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '@ 30 years'::interval)
(4 rows)
@@ -951,21 +951,21 @@ CREATE INDEX ON brin_interval_test USING brin (a interval_minmax_multi_ops) WITH
SET enable_seqscan = off;
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_interval_test WHERE a = '-30 years'::interval;
- QUERY PLAN
------------------------------------------------------------------------------
- Bitmap Heap Scan on brin_interval_test (actual rows=0 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_interval_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '@ 30 years ago'::interval)
- -> Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '@ 30 years ago'::interval)
(4 rows)
EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, BUFFERS OFF)
SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
- QUERY PLAN
------------------------------------------------------------------------------
- Bitmap Heap Scan on brin_interval_test (actual rows=0 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Bitmap Heap Scan on brin_interval_test (actual rows=0.00 loops=1)
Recheck Cond: (a = '@ 30 years'::interval)
- -> Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on brin_interval_test_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = '@ 30 years'::interval)
(4 rows)
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 97eb4b76526..f5d60e50893 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -61,26 +61,26 @@ select explain_filter('explain select * from int8_tbl i8');
(1 row)
select explain_filter('explain (analyze, buffers off) select * from int8_tbl i8');
- explain_filter
------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+-------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Execution Time: N.N ms
(3 rows)
select explain_filter('explain (analyze, buffers off, verbose) select * from int8_tbl i8');
- explain_filter
-------------------------------------------------------------------------------------------------------
- Seq Scan on public.int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+--------------------------------------------------------------------------------------------------------
+ Seq Scan on public.int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Output: q1, q2
Planning Time: N.N ms
Execution Time: N.N ms
(4 rows)
select explain_filter('explain (analyze, buffers, format text) select * from int8_tbl i8');
- explain_filter
------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+-------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Execution Time: N.N ms
(3 rows)
@@ -102,7 +102,7 @@ select explain_filter('explain (analyze, buffers, format xml) select * from int8
<Plan-Width>N</Plan-Width> +
<Actual-Startup-Time>N.N</Actual-Startup-Time> +
<Actual-Total-Time>N.N</Actual-Total-Time> +
- <Actual-Rows>N</Actual-Rows> +
+ <Actual-Rows>N.N</Actual-Rows> +
<Actual-Loops>N</Actual-Loops> +
<Disabled>false</Disabled> +
<Shared-Hit-Blocks>N</Shared-Hit-Blocks> +
@@ -151,7 +151,7 @@ select explain_filter('explain (analyze, serialize, buffers, format yaml) select
Plan Width: N +
Actual Startup Time: N.N +
Actual Total Time: N.N +
- Actual Rows: N +
+ Actual Rows: N.N +
Actual Loops: N +
Disabled: false +
Shared Hit Blocks: N +
@@ -263,7 +263,7 @@ select explain_filter('explain (analyze, buffers, format json) select * from int
"Plan Width": N, +
"Actual Startup Time": N.N, +
"Actual Total Time": N.N, +
- "Actual Rows": N, +
+ "Actual Rows": N.N, +
"Actual Loops": N, +
"Disabled": false, +
"Shared Hit Blocks": N, +
@@ -353,9 +353,9 @@ select explain_filter('explain (memory) select * from int8_tbl i8');
(2 rows)
select explain_filter('explain (memory, analyze, buffers off) select * from int8_tbl i8');
- explain_filter
------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+-------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Memory: used=NkB allocated=NkB
Planning Time: N.N ms
Execution Time: N.N ms
@@ -398,7 +398,7 @@ select explain_filter('explain (memory, analyze, format json) select * from int8
"Plan Width": N, +
"Actual Startup Time": N.N, +
"Actual Total Time": N.N, +
- "Actual Rows": N, +
+ "Actual Rows": N.N, +
"Actual Loops": N, +
"Disabled": false, +
"Shared Hit Blocks": N, +
@@ -493,9 +493,6 @@ select jsonb_pretty(
-- Also remove its sort-type fields, as those aren't 100% stable
#- '{0,Plan,Plans,0,Sort Method}'
#- '{0,Plan,Plans,0,Sort Space Type}'
- -- Actual Rows can be 0 or 0.0 depending on whether loops>1
- #- '{0,Plan,Plans,0,Actual Rows}'
- #- '{0,Plan,Plans,0,Plans,0,Actual Rows}'
);
jsonb_pretty
-------------------------------------------------------------
@@ -531,6 +528,7 @@ select jsonb_pretty(
"Plan Rows": 0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
+ "Actual Rows": 0.0, +
"Actual Loops": 0, +
"Startup Cost": 0.0, +
"Async Capable": false, +
@@ -577,6 +575,7 @@ select jsonb_pretty(
"Plan Rows": 0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
+ "Actual Rows": 0.0, +
"Actual Loops": 0, +
"Startup Cost": 0.0, +
"Async Capable": false, +
@@ -620,7 +619,7 @@ select jsonb_pretty(
"Plan Rows": 0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
- "Actual Rows": 0, +
+ "Actual Rows": 0.0, +
"Actual Loops": 0, +
"Startup Cost": 0.0, +
"Async Capable": false, +
@@ -702,27 +701,27 @@ select explain_filter('explain (verbose) create table test_ctas as select 1');
-- Test SERIALIZE option
select explain_filter('explain (analyze,buffers off,serialize) select * from int8_tbl i8');
- explain_filter
------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+-------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Serialization: time=N.N ms output=NkB format=text
Execution Time: N.N ms
(4 rows)
select explain_filter('explain (analyze,serialize text,buffers,timing off) select * from int8_tbl i8');
- explain_filter
----------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual rows=N loops=N)
+ explain_filter
+-----------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual rows=N.N loops=N)
Planning Time: N.N ms
Serialization: output=NkB format=text
Execution Time: N.N ms
(4 rows)
select explain_filter('explain (analyze,serialize binary,buffers,timing) select * from int8_tbl i8');
- explain_filter
------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+-------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Serialization: time=N.N ms output=NkB format=binary
Execution Time: N.N ms
@@ -730,9 +729,9 @@ select explain_filter('explain (analyze,serialize binary,buffers,timing) select
-- this tests an edge case where we have no data to return
select explain_filter('explain (analyze,buffers off,serialize) create temp table explain_temp as select * from int8_tbl i8');
- explain_filter
------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+-------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Serialization: time=N.N ms output=NkB format=text
Execution Time: N.N ms
@@ -740,11 +739,11 @@ select explain_filter('explain (analyze,buffers off,serialize) create temp table
-- Test tuplestore storage usage in Window aggregate (memory case)
select explain_filter('explain (analyze,buffers off,costs off) select sum(n) over() from generate_series(1,10) a(n)');
- explain_filter
---------------------------------------------------------------------------------
- WindowAgg (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+----------------------------------------------------------------------------------
+ WindowAgg (actual time=N.N..N.N rows=N.N loops=N)
Storage: Memory Maximum Storage: NkB
- -> Function Scan on generate_series a (actual time=N.N..N.N rows=N loops=N)
+ -> Function Scan on generate_series a (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Execution Time: N.N ms
(5 rows)
@@ -752,25 +751,25 @@ select explain_filter('explain (analyze,buffers off,costs off) select sum(n) ove
-- Test tuplestore storage usage in Window aggregate (disk case)
set work_mem to 64;
select explain_filter('explain (analyze,buffers off,costs off) select sum(n) over() from generate_series(1,2000) a(n)');
- explain_filter
---------------------------------------------------------------------------------
- WindowAgg (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+----------------------------------------------------------------------------------
+ WindowAgg (actual time=N.N..N.N rows=N.N loops=N)
Storage: Disk Maximum Storage: NkB
- -> Function Scan on generate_series a (actual time=N.N..N.N rows=N loops=N)
+ -> Function Scan on generate_series a (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Execution Time: N.N ms
(5 rows)
-- Test tuplestore storage usage in Window aggregate (memory and disk case, final result is disk)
select explain_filter('explain (analyze,buffers off,costs off) select sum(n) over(partition by m) from (SELECT n < 3 as m, n from generate_series(1,2000) a(n))');
- explain_filter
---------------------------------------------------------------------------------------
- WindowAgg (actual time=N.N..N.N rows=N loops=N)
+ explain_filter
+----------------------------------------------------------------------------------------
+ WindowAgg (actual time=N.N..N.N rows=N.N loops=N)
Storage: Disk Maximum Storage: NkB
- -> Sort (actual time=N.N..N.N rows=N loops=N)
+ -> Sort (actual time=N.N..N.N rows=N.N loops=N)
Sort Key: ((a.n < N))
Sort Method: external merge Disk: NkB
- -> Function Scan on generate_series a (actual time=N.N..N.N rows=N loops=N)
+ -> Function Scan on generate_series a (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Execution Time: N.N ms
(8 rows)
diff --git a/src/test/regress/expected/gin.out b/src/test/regress/expected/gin.out
index 0af464309ee..118bc26d8cd 100644
--- a/src/test/regress/expected/gin.out
+++ b/src/test/regress/expected/gin.out
@@ -187,16 +187,16 @@ from
lateral execute_text_query_heap($$select string_agg((i, j)::text, ' ') from t_gin_test_tbl where $$ || query) res_heap;
query | return by index | removed by recheck | match
-------------------------------------------+-----------------+--------------------+-------
- i @> '{}' | 7 | 0 | t
- j @> '{}' | 6 | 0 | t
- i @> '{}' and j @> '{}' | 4 | 0 | t
- i @> '{1}' | 5 | 0 | t
- i @> '{1}' and j @> '{}' | 3 | 0 | t
- i @> '{1}' and i @> '{}' and j @> '{}' | 3 | 0 | t
- j @> '{10}' | 4 | 0 | t
- j @> '{10}' and i @> '{}' | 3 | 0 | t
- j @> '{10}' and j @> '{}' and i @> '{}' | 3 | 0 | t
- i @> '{1}' and j @> '{10}' | 2 | 0 | t
+ i @> '{}' | 7.00 | 0 | t
+ j @> '{}' | 6.00 | 0 | t
+ i @> '{}' and j @> '{}' | 4.00 | 0 | t
+ i @> '{1}' | 5.00 | 0 | t
+ i @> '{1}' and j @> '{}' | 3.00 | 0 | t
+ i @> '{1}' and i @> '{}' and j @> '{}' | 3.00 | 0 | t
+ j @> '{10}' | 4.00 | 0 | t
+ j @> '{10}' and i @> '{}' | 3.00 | 0 | t
+ j @> '{10}' and j @> '{}' and i @> '{}' | 3.00 | 0 | t
+ i @> '{1}' and j @> '{10}' | 2.00 | 0 | t
(10 rows)
reset enable_seqscan;
diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.out
index d5975758409..b00219643b9 100644
--- a/src/test/regress/expected/incremental_sort.out
+++ b/src/test/regress/expected/incremental_sort.out
@@ -522,15 +522,15 @@ select * from (select * from t order by a) s order by a, b limit 55;
select explain_analyze_without_memory('select * from (select * from t order by a) s order by a, b limit 55');
explain_analyze_without_memory
---------------------------------------------------------------------------------------------------------------
- Limit (actual rows=55 loops=1)
- -> Incremental Sort (actual rows=55 loops=1)
+ Limit (actual rows=55.00 loops=1)
+ -> Incremental Sort (actual rows=55.00 loops=1)
Sort Key: t.a, t.b
Presorted Key: t.a
Full-sort Groups: 2 Sort Methods: top-N heapsort, quicksort Average Memory: NNkB Peak Memory: NNkB
- -> Sort (actual rows=101 loops=1)
+ -> Sort (actual rows=101.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: NNkB
- -> Seq Scan on t (actual rows=1000 loops=1)
+ -> Seq Scan on t (actual rows=1000.00 loops=1)
(9 rows)
select jsonb_pretty(explain_analyze_inc_sort_nodes_without_memory('select * from (select * from t order by a) s order by a, b limit 55'));
@@ -544,7 +544,7 @@ select jsonb_pretty(explain_analyze_inc_sort_nodes_without_memory('select * from
"t.b" +
], +
"Node Type": "Incremental Sort", +
- "Actual Rows": 55, +
+ "Actual Rows": 55.00, +
"Actual Loops": 1, +
"Async Capable": false, +
"Presorted Key": [ +
@@ -726,16 +726,16 @@ rollback;
select explain_analyze_without_memory('select * from (select * from t order by a) s order by a, b limit 70');
explain_analyze_without_memory
----------------------------------------------------------------------------------------------------------------
- Limit (actual rows=70 loops=1)
- -> Incremental Sort (actual rows=70 loops=1)
+ Limit (actual rows=70.00 loops=1)
+ -> Incremental Sort (actual rows=70.00 loops=1)
Sort Key: t.a, t.b
Presorted Key: t.a
Full-sort Groups: 1 Sort Method: quicksort Average Memory: NNkB Peak Memory: NNkB
Pre-sorted Groups: 5 Sort Methods: top-N heapsort, quicksort Average Memory: NNkB Peak Memory: NNkB
- -> Sort (actual rows=1000 loops=1)
+ -> Sort (actual rows=1000.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: NNkB
- -> Seq Scan on t (actual rows=1000 loops=1)
+ -> Seq Scan on t (actual rows=1000.00 loops=1)
(10 rows)
select jsonb_pretty(explain_analyze_inc_sort_nodes_without_memory('select * from (select * from t order by a) s order by a, b limit 70'));
@@ -749,7 +749,7 @@ select jsonb_pretty(explain_analyze_inc_sort_nodes_without_memory('select * from
"t.b" +
], +
"Node Type": "Incremental Sort", +
- "Actual Rows": 70, +
+ "Actual Rows": 70.00, +
"Actual Loops": 1, +
"Async Capable": false, +
"Presorted Key": [ +
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 9eab51bc2a6..54939ecc6b0 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -627,10 +627,10 @@ CREATE MATERIALIZED VIEW matview_schema.mv_withdata1 (a) AS
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
CREATE MATERIALIZED VIEW matview_schema.mv_withdata2 (a) AS
SELECT generate_series(1, 10) WITH DATA;
- QUERY PLAN
---------------------------------------
- ProjectSet (actual rows=10 loops=1)
- -> Result (actual rows=1 loops=1)
+ QUERY PLAN
+-----------------------------------------
+ ProjectSet (actual rows=10.00 loops=1)
+ -> Result (actual rows=1.00 loops=1)
(2 rows)
REFRESH MATERIALIZED VIEW matview_schema.mv_withdata2;
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index dbd01066d00..22f2d32845e 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -37,9 +37,9 @@ INNER JOIN tenk1 t2 ON t1.unique1 = t2.twenty
WHERE t2.unique1 < 1000;', false);
explain_memoize
----------------------------------------------------------------------------------------------
- Aggregate (actual rows=1 loops=N)
- -> Nested Loop (actual rows=1000 loops=N)
- -> Seq Scan on tenk1 t2 (actual rows=1000 loops=N)
+ Aggregate (actual rows=1.00 loops=N)
+ -> Nested Loop (actual rows=1000.00 loops=N)
+ -> Seq Scan on tenk1 t2 (actual rows=1000.00 loops=N)
Filter: (unique1 < 1000)
Rows Removed by Filter: 9000
-> Memoize (actual rows=1.00 loops=N)
@@ -68,9 +68,9 @@ LATERAL (SELECT t2.unique1 FROM tenk1 t2
WHERE t1.unique1 < 1000;', false);
explain_memoize
----------------------------------------------------------------------------------------------
- Aggregate (actual rows=1 loops=N)
- -> Nested Loop (actual rows=1000 loops=N)
- -> Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+ Aggregate (actual rows=1.00 loops=N)
+ -> Nested Loop (actual rows=1000.00 loops=N)
+ -> Seq Scan on tenk1 t1 (actual rows=1000.00 loops=N)
Filter: (unique1 < 1000)
Rows Removed by Filter: 9000
-> Memoize (actual rows=1.00 loops=N)
@@ -102,9 +102,9 @@ ON t1.two = t2.two
WHERE t1.unique1 < 10;', false);
explain_memoize
-------------------------------------------------------------------------------------------------
- Aggregate (actual rows=1 loops=N)
- -> Nested Loop Left Join (actual rows=20 loops=N)
- -> Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10 loops=N)
+ Aggregate (actual rows=1.00 loops=N)
+ -> Nested Loop Left Join (actual rows=20.00 loops=N)
+ -> Index Scan using tenk1_unique1 on tenk1 t1 (actual rows=10.00 loops=N)
Index Cond: (unique1 < 10)
-> Memoize (actual rows=2.00 loops=N)
Cache Key: t1.two
@@ -136,9 +136,9 @@ LATERAL (SELECT t1.two+1 AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
explain_memoize
----------------------------------------------------------------------------------------------
- Aggregate (actual rows=1 loops=N)
- -> Nested Loop (actual rows=1000 loops=N)
- -> Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+ Aggregate (actual rows=1.00 loops=N)
+ -> Nested Loop (actual rows=1000.00 loops=N)
+ -> Seq Scan on tenk1 t1 (actual rows=1000.00 loops=N)
Filter: (unique1 < 1000)
Rows Removed by Filter: 9000
-> Memoize (actual rows=1.00 loops=N)
@@ -168,9 +168,9 @@ ON t1.two = s.two
WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
explain_memoize
---------------------------------------------------------------------------------------
- Aggregate (actual rows=1 loops=N)
- -> Nested Loop (actual rows=1000 loops=N)
- -> Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+ Aggregate (actual rows=1.00 loops=N)
+ -> Nested Loop (actual rows=1000.00 loops=N)
+ -> Seq Scan on tenk1 t1 (actual rows=1000.00 loops=N)
Filter: (unique1 < 1000)
Rows Removed by Filter: 9000
-> Memoize (actual rows=1.00 loops=N)
@@ -209,8 +209,8 @@ SELECT * FROM expr_key t1 INNER JOIN expr_key t2
ON t1.x = t2.t::numeric AND t1.t::numeric = t2.x;', false);
explain_memoize
----------------------------------------------------------------------------------------------
- Nested Loop (actual rows=80 loops=N)
- -> Seq Scan on expr_key t1 (actual rows=40 loops=N)
+ Nested Loop (actual rows=80.00 loops=N)
+ -> Seq Scan on expr_key t1 (actual rows=40.00 loops=N)
-> Memoize (actual rows=2.00 loops=N)
Cache Key: t1.x, (t1.t)::numeric
Cache Mode: logical
@@ -234,9 +234,9 @@ INNER JOIN tenk1 t2 ON t1.unique1 = t2.thousand
WHERE t2.unique1 < 1200;', true);
explain_memoize
----------------------------------------------------------------------------------------------
- Aggregate (actual rows=1 loops=N)
- -> Nested Loop (actual rows=1200 loops=N)
- -> Seq Scan on tenk1 t2 (actual rows=1200 loops=N)
+ Aggregate (actual rows=1.00 loops=N)
+ -> Nested Loop (actual rows=1200.00 loops=N)
+ -> Seq Scan on tenk1 t2 (actual rows=1200.00 loops=N)
Filter: (unique1 < 1200)
Rows Removed by Filter: 8800
-> Memoize (actual rows=1.00 loops=N)
@@ -256,16 +256,16 @@ SET enable_seqscan TO off;
-- Ensure memoize operates in logical mode
SELECT explain_memoize('
SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false);
- explain_memoize
--------------------------------------------------------------------------------
- Nested Loop (actual rows=4 loops=N)
- -> Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+ explain_memoize
+----------------------------------------------------------------------------------
+ Nested Loop (actual rows=4.00 loops=N)
+ -> Index Only Scan using flt_f_idx on flt f1 (actual rows=2.00 loops=N)
Heap Fetches: N
-> Memoize (actual rows=2.00 loops=N)
Cache Key: f1.f
Cache Mode: logical
Hits: 1 Misses: 1 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using flt_f_idx on flt f2 (actual rows=2 loops=N)
+ -> Index Only Scan using flt_f_idx on flt f2 (actual rows=2.00 loops=N)
Index Cond: (f = f1.f)
Heap Fetches: N
(10 rows)
@@ -275,8 +275,8 @@ SELECT explain_memoize('
SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f >= f2.f;', false);
explain_memoize
----------------------------------------------------------------------------------
- Nested Loop (actual rows=4 loops=N)
- -> Index Only Scan using flt_f_idx on flt f1 (actual rows=2 loops=N)
+ Nested Loop (actual rows=4.00 loops=N)
+ -> Index Only Scan using flt_f_idx on flt f1 (actual rows=2.00 loops=N)
Heap Fetches: N
-> Memoize (actual rows=2.00 loops=N)
Cache Key: f1.f
@@ -302,8 +302,8 @@ SELECT explain_memoize('
SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.n >= s2.n;', false);
explain_memoize
-------------------------------------------------------------------------------------
- Nested Loop (actual rows=24 loops=N)
- -> Seq Scan on strtest s1 (actual rows=6 loops=N)
+ Nested Loop (actual rows=24.00 loops=N)
+ -> Seq Scan on strtest s1 (actual rows=6.00 loops=N)
Disabled: true
-> Memoize (actual rows=4.00 loops=N)
Cache Key: s1.n
@@ -318,8 +318,8 @@ SELECT explain_memoize('
SELECT * FROM strtest s1 INNER JOIN strtest s2 ON s1.t >= s2.t;', false);
explain_memoize
-------------------------------------------------------------------------------------
- Nested Loop (actual rows=24 loops=N)
- -> Seq Scan on strtest s1 (actual rows=6 loops=N)
+ Nested Loop (actual rows=24.00 loops=N)
+ -> Seq Scan on strtest s1 (actual rows=6.00 loops=N)
Disabled: true
-> Memoize (actual rows=4.00 loops=N)
Cache Key: s1.t
@@ -342,27 +342,27 @@ CREATE INDEX iprt_p2_a ON prt_p2 (a);
ANALYZE prt;
SELECT explain_memoize('
SELECT * FROM prt t1 INNER JOIN prt t2 ON t1.a = t2.a;', false);
- explain_memoize
-------------------------------------------------------------------------------------------
- Append (actual rows=32 loops=N)
- -> Nested Loop (actual rows=16 loops=N)
- -> Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4 loops=N)
+ explain_memoize
+---------------------------------------------------------------------------------------------
+ Append (actual rows=32.00 loops=N)
+ -> Nested Loop (actual rows=16.00 loops=N)
+ -> Index Only Scan using iprt_p1_a on prt_p1 t1_1 (actual rows=4.00 loops=N)
Heap Fetches: N
-> Memoize (actual rows=4.00 loops=N)
Cache Key: t1_1.a
Cache Mode: logical
Hits: 3 Misses: 1 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4 loops=N)
+ -> Index Only Scan using iprt_p1_a on prt_p1 t2_1 (actual rows=4.00 loops=N)
Index Cond: (a = t1_1.a)
Heap Fetches: N
- -> Nested Loop (actual rows=16 loops=N)
- -> Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4 loops=N)
+ -> Nested Loop (actual rows=16.00 loops=N)
+ -> Index Only Scan using iprt_p2_a on prt_p2 t1_2 (actual rows=4.00 loops=N)
Heap Fetches: N
-> Memoize (actual rows=4.00 loops=N)
Cache Key: t1_2.a
Cache Mode: logical
Hits: 3 Misses: 1 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4 loops=N)
+ -> Index Only Scan using iprt_p2_a on prt_p2 t2_2 (actual rows=4.00 loops=N)
Index Cond: (a = t1_2.a)
Heap Fetches: N
(21 rows)
@@ -373,20 +373,20 @@ SELECT explain_memoize('
SELECT * FROM prt_p1 t1 INNER JOIN
(SELECT * FROM prt_p1 UNION ALL SELECT * FROM prt_p2) t2
ON t1.a = t2.a;', false);
- explain_memoize
--------------------------------------------------------------------------------------
- Nested Loop (actual rows=16 loops=N)
- -> Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4 loops=N)
+ explain_memoize
+----------------------------------------------------------------------------------------
+ Nested Loop (actual rows=16.00 loops=N)
+ -> Index Only Scan using iprt_p1_a on prt_p1 t1 (actual rows=4.00 loops=N)
Heap Fetches: N
-> Memoize (actual rows=4.00 loops=N)
Cache Key: t1.a
Cache Mode: logical
Hits: 3 Misses: 1 Evictions: Zero Overflows: 0 Memory Usage: NkB
- -> Append (actual rows=4 loops=N)
- -> Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4 loops=N)
+ -> Append (actual rows=4.00 loops=N)
+ -> Index Only Scan using iprt_p1_a on prt_p1 (actual rows=4.00 loops=N)
Index Cond: (a = t1.a)
Heap Fetches: N
- -> Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0 loops=N)
+ -> Index Only Scan using iprt_p2_a on prt_p2 (actual rows=0.00 loops=N)
Index Cond: (a = t1.a)
Heap Fetches: N
(14 rows)
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 05314ad4397..c8ecc8b6b4a 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -1636,20 +1636,20 @@ SELECT explain_merge('
MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = t.b + 1');
- explain_merge
-----------------------------------------------------------------------
- Merge on ex_mtarget t (actual rows=0 loops=1)
+ explain_merge
+-------------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0.00 loops=1)
Tuples: updated=50
- -> Merge Join (actual rows=50 loops=1)
+ -> Merge Join (actual rows=50.00 loops=1)
Merge Cond: (t.a = s.a)
- -> Sort (actual rows=50 loops=1)
+ -> Sort (actual rows=50.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_mtarget t (actual rows=50 loops=1)
- -> Sort (actual rows=100 loops=1)
+ -> Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
+ -> Sort (actual rows=100.00 loops=1)
Sort Key: s.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_msource s (actual rows=100 loops=1)
+ -> Seq Scan on ex_msource s (actual rows=100.00 loops=1)
(12 rows)
-- only updates to selected tuples
@@ -1657,20 +1657,20 @@ SELECT explain_merge('
MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
WHEN MATCHED AND t.a < 10 THEN
UPDATE SET b = t.b + 1');
- explain_merge
-----------------------------------------------------------------------
- Merge on ex_mtarget t (actual rows=0 loops=1)
+ explain_merge
+-------------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0.00 loops=1)
Tuples: updated=5 skipped=45
- -> Merge Join (actual rows=50 loops=1)
+ -> Merge Join (actual rows=50.00 loops=1)
Merge Cond: (t.a = s.a)
- -> Sort (actual rows=50 loops=1)
+ -> Sort (actual rows=50.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_mtarget t (actual rows=50 loops=1)
- -> Sort (actual rows=100 loops=1)
+ -> Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
+ -> Sort (actual rows=100.00 loops=1)
Sort Key: s.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_msource s (actual rows=100 loops=1)
+ -> Seq Scan on ex_msource s (actual rows=100.00 loops=1)
(12 rows)
-- updates + deletes
@@ -1680,20 +1680,20 @@ WHEN MATCHED AND t.a < 10 THEN
UPDATE SET b = t.b + 1
WHEN MATCHED AND t.a >= 10 AND t.a <= 20 THEN
DELETE');
- explain_merge
-----------------------------------------------------------------------
- Merge on ex_mtarget t (actual rows=0 loops=1)
+ explain_merge
+-------------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0.00 loops=1)
Tuples: updated=5 deleted=5 skipped=40
- -> Merge Join (actual rows=50 loops=1)
+ -> Merge Join (actual rows=50.00 loops=1)
Merge Cond: (t.a = s.a)
- -> Sort (actual rows=50 loops=1)
+ -> Sort (actual rows=50.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_mtarget t (actual rows=50 loops=1)
- -> Sort (actual rows=100 loops=1)
+ -> Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
+ -> Sort (actual rows=100.00 loops=1)
Sort Key: s.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_msource s (actual rows=100 loops=1)
+ -> Seq Scan on ex_msource s (actual rows=100.00 loops=1)
(12 rows)
-- only inserts
@@ -1701,20 +1701,20 @@ SELECT explain_merge('
MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
WHEN NOT MATCHED AND s.a < 10 THEN
INSERT VALUES (a, b)');
- explain_merge
-----------------------------------------------------------------------
- Merge on ex_mtarget t (actual rows=0 loops=1)
+ explain_merge
+-------------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0.00 loops=1)
Tuples: inserted=4 skipped=96
- -> Merge Left Join (actual rows=100 loops=1)
+ -> Merge Left Join (actual rows=100.00 loops=1)
Merge Cond: (s.a = t.a)
- -> Sort (actual rows=100 loops=1)
+ -> Sort (actual rows=100.00 loops=1)
Sort Key: s.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_msource s (actual rows=100 loops=1)
- -> Sort (actual rows=45 loops=1)
+ -> Seq Scan on ex_msource s (actual rows=100.00 loops=1)
+ -> Sort (actual rows=45.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_mtarget t (actual rows=45 loops=1)
+ -> Seq Scan on ex_mtarget t (actual rows=45.00 loops=1)
(12 rows)
-- all three
@@ -1726,20 +1726,20 @@ WHEN MATCHED AND t.a >= 30 AND t.a <= 40 THEN
DELETE
WHEN NOT MATCHED AND s.a < 20 THEN
INSERT VALUES (a, b)');
- explain_merge
-----------------------------------------------------------------------
- Merge on ex_mtarget t (actual rows=0 loops=1)
+ explain_merge
+-------------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0.00 loops=1)
Tuples: inserted=10 updated=9 deleted=5 skipped=76
- -> Merge Left Join (actual rows=100 loops=1)
+ -> Merge Left Join (actual rows=100.00 loops=1)
Merge Cond: (s.a = t.a)
- -> Sort (actual rows=100 loops=1)
+ -> Sort (actual rows=100.00 loops=1)
Sort Key: s.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_msource s (actual rows=100 loops=1)
- -> Sort (actual rows=49 loops=1)
+ -> Seq Scan on ex_msource s (actual rows=100.00 loops=1)
+ -> Sort (actual rows=49.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_mtarget t (actual rows=49 loops=1)
+ -> Seq Scan on ex_mtarget t (actual rows=49.00 loops=1)
(12 rows)
-- not matched by source
@@ -1747,20 +1747,20 @@ SELECT explain_merge('
MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
WHEN NOT MATCHED BY SOURCE and t.a < 10 THEN
DELETE');
- explain_merge
-----------------------------------------------------------------------
- Merge on ex_mtarget t (actual rows=0 loops=1)
+ explain_merge
+-------------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0.00 loops=1)
Tuples: skipped=54
- -> Merge Left Join (actual rows=54 loops=1)
+ -> Merge Left Join (actual rows=54.00 loops=1)
Merge Cond: (t.a = s.a)
- -> Sort (actual rows=54 loops=1)
+ -> Sort (actual rows=54.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_mtarget t (actual rows=54 loops=1)
- -> Sort (actual rows=100 loops=1)
+ -> Seq Scan on ex_mtarget t (actual rows=54.00 loops=1)
+ -> Sort (actual rows=100.00 loops=1)
Sort Key: s.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_msource s (actual rows=100 loops=1)
+ -> Seq Scan on ex_msource s (actual rows=100.00 loops=1)
(12 rows)
-- not matched by source and target
@@ -1770,20 +1770,20 @@ WHEN NOT MATCHED BY SOURCE AND t.a < 10 THEN
DELETE
WHEN NOT MATCHED BY TARGET AND s.a < 20 THEN
INSERT VALUES (a, b)');
- explain_merge
-----------------------------------------------------------------------
- Merge on ex_mtarget t (actual rows=0 loops=1)
+ explain_merge
+-------------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0.00 loops=1)
Tuples: skipped=100
- -> Merge Full Join (actual rows=100 loops=1)
+ -> Merge Full Join (actual rows=100.00 loops=1)
Merge Cond: (t.a = s.a)
- -> Sort (actual rows=54 loops=1)
+ -> Sort (actual rows=54.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_mtarget t (actual rows=54 loops=1)
- -> Sort (actual rows=100 loops=1)
+ -> Seq Scan on ex_mtarget t (actual rows=54.00 loops=1)
+ -> Sort (actual rows=100.00 loops=1)
Sort Key: s.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_msource s (actual rows=100 loops=1)
+ -> Seq Scan on ex_msource s (actual rows=100.00 loops=1)
(12 rows)
-- nothing
@@ -1791,15 +1791,15 @@ SELECT explain_merge('
MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a AND t.a < -1000
WHEN MATCHED AND t.a < 10 THEN
DO NOTHING');
- explain_merge
---------------------------------------------------------------------
- Merge on ex_mtarget t (actual rows=0 loops=1)
- -> Merge Join (actual rows=0 loops=1)
+ explain_merge
+-----------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0.00 loops=1)
+ -> Merge Join (actual rows=0.00 loops=1)
Merge Cond: (t.a = s.a)
- -> Sort (actual rows=0 loops=1)
+ -> Sort (actual rows=0.00 loops=1)
Sort Key: t.a
Sort Method: quicksort Memory: xxx
- -> Seq Scan on ex_mtarget t (actual rows=0 loops=1)
+ -> Seq Scan on ex_mtarget t (actual rows=0.00 loops=1)
Filter: (a < '-1000'::integer)
Rows Removed by Filter: 54
-> Sort (never executed)
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index 106dedb519a..ad30ad0f26f 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -647,27 +647,27 @@ SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g;
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
-------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30 loops=1)
+ explain_mask_costs
+---------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1)
(1 row)
-- As above but with generate_series_timestamp
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMP '2024-02-01', TIMESTAMP '2024-03-01', INTERVAL '1 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
-------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30 loops=1)
+ explain_mask_costs
+---------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1)
(1 row)
-- As above but with generate_series_timestamptz_at_zone()
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day', 'UTC') g(s);$$,
true, true, false, true);
- explain_mask_costs
-------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30 loops=1)
+ explain_mask_costs
+---------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1)
(1 row)
-- Ensure the estimated and actual row counts match when the range isn't
@@ -675,27 +675,27 @@ true, true, false, true);
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '7 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
-----------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=5 width=N) (actual rows=5 loops=1)
+ explain_mask_costs
+-------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1)
(1 row)
-- Ensure the estimates match when step is decreasing
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '-1 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
-------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30 loops=1)
+ explain_mask_costs
+---------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1)
(1 row)
-- Ensure an empty range estimates 1 row
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '1 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
-----------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0 loops=1)
+ explain_mask_costs
+-------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
(1 row)
-- Ensure we get the default row estimate for infinity values
@@ -719,36 +719,36 @@ ERROR: step size cannot equal zero
SELECT explain_mask_costs($$
SELECT * FROM generate_series(1.0, 25.0) g(s);$$,
true, true, false, true);
- explain_mask_costs
-------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25 loops=1)
+ explain_mask_costs
+---------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1)
(1 row)
-- As above but with non-default step
SELECT explain_mask_costs($$
SELECT * FROM generate_series(1.0, 25.0, 2.0) g(s);$$,
true, true, false, true);
- explain_mask_costs
-------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=13 width=N) (actual rows=13 loops=1)
+ explain_mask_costs
+---------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=13 width=N) (actual rows=13.00 loops=1)
(1 row)
-- Ensure the estimates match when step is decreasing
SELECT explain_mask_costs($$
SELECT * FROM generate_series(25.0, 1.0, -1.0) g(s);$$,
true, true, false, true);
- explain_mask_costs
-------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25 loops=1)
+ explain_mask_costs
+---------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1)
(1 row)
-- Ensure an empty range estimates 1 row
SELECT explain_mask_costs($$
SELECT * FROM generate_series(25.0, 1.0, 1.0) g(s);$$,
true, true, false, true);
- explain_mask_costs
-----------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0 loops=1)
+ explain_mask_costs
+-------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
(1 row)
-- Ensure we get the default row estimate for error cases (infinity/NaN values
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 1dbe6ff54fb..3d8cfebae61 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2157,34 +2157,34 @@ set enable_indexonlyscan = off;
prepare ab_q1 (int, int, int) as
select * from ab where a between $1 and $2 and b <= $3;
explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q1 (2, 2, 3);
- QUERY PLAN
----------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 6
- -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
- -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
- -> Seq Scan on ab_a2_b3 ab_3 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b3 ab_3 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
(8 rows)
explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q1 (1, 2, 3);
- QUERY PLAN
----------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 3
- -> Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
- -> Seq Scan on ab_a1_b2 ab_2 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a1_b2 ab_2 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
- -> Seq Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a1_b3 ab_3 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
- -> Seq Scan on ab_a2_b1 ab_4 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
- -> Seq Scan on ab_a2_b2 ab_5 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b2 ab_5 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
- -> Seq Scan on ab_a2_b3 ab_6 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b3 ab_6 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b <= $3))
(14 rows)
@@ -2193,28 +2193,28 @@ deallocate ab_q1;
prepare ab_q1 (int, int) as
select a from ab where a between $1 and $2 and b < 3;
explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q1 (2, 2);
- QUERY PLAN
----------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 4
- -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
- -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
(6 rows)
explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q1 (2, 4);
- QUERY PLAN
----------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 2
- -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
- -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
- -> Seq Scan on ab_a3_b1 ab_3 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a3_b1 ab_3 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
- -> Seq Scan on ab_a3_b2 ab_4 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a3_b2 ab_4 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
(10 rows)
@@ -2225,13 +2225,13 @@ select a from ab where a between $1 and $2 and b < (select 3);
explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q2 (2, 2);
QUERY PLAN
-----------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 6
InitPlan 1
- -> Result (actual rows=1 loops=1)
- -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0 loops=1)
+ -> Result (actual rows=1.00 loops=1)
+ -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1))
- -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1)
Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1))
-> Seq Scan on ab_a2_b3 ab_3 (never executed)
Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1))
@@ -2243,13 +2243,13 @@ select a from ab where b between $1 and $2 and a < (select 3);
explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q3 (2, 2);
QUERY PLAN
-----------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 6
InitPlan 1
- -> Result (actual rows=1 loops=1)
- -> Seq Scan on ab_a1_b2 ab_1 (actual rows=0 loops=1)
+ -> Result (actual rows=1.00 loops=1)
+ -> Seq Scan on ab_a1_b2 ab_1 (actual rows=0.00 loops=1)
Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1))
- -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1)
Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1))
-> Seq Scan on ab_a3_b2 ab_3 (never executed)
Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1))
@@ -2303,44 +2303,44 @@ begin;
create function list_part_fn(int) returns int as $$ begin return $1; end;$$ language plpgsql stable;
-- Ensure pruning works using a stable function containing no Vars
explain (analyze, costs off, summary off, timing off, buffers off) select * from list_part where a = list_part_fn(1);
- QUERY PLAN
-------------------------------------------------------------------
- Append (actual rows=1 loops=1)
+ QUERY PLAN
+---------------------------------------------------------------------
+ Append (actual rows=1.00 loops=1)
Subplans Removed: 3
- -> Seq Scan on list_part1 list_part_1 (actual rows=1 loops=1)
+ -> Seq Scan on list_part1 list_part_1 (actual rows=1.00 loops=1)
Filter: (a = list_part_fn(1))
(4 rows)
-- Ensure pruning does not take place when the function has a Var parameter
explain (analyze, costs off, summary off, timing off, buffers off) select * from list_part where a = list_part_fn(a);
- QUERY PLAN
-------------------------------------------------------------------
- Append (actual rows=4 loops=1)
- -> Seq Scan on list_part1 list_part_1 (actual rows=1 loops=1)
+ QUERY PLAN
+---------------------------------------------------------------------
+ Append (actual rows=4.00 loops=1)
+ -> Seq Scan on list_part1 list_part_1 (actual rows=1.00 loops=1)
Filter: (a = list_part_fn(a))
- -> Seq Scan on list_part2 list_part_2 (actual rows=1 loops=1)
+ -> Seq Scan on list_part2 list_part_2 (actual rows=1.00 loops=1)
Filter: (a = list_part_fn(a))
- -> Seq Scan on list_part3 list_part_3 (actual rows=1 loops=1)
+ -> Seq Scan on list_part3 list_part_3 (actual rows=1.00 loops=1)
Filter: (a = list_part_fn(a))
- -> Seq Scan on list_part4 list_part_4 (actual rows=1 loops=1)
+ -> Seq Scan on list_part4 list_part_4 (actual rows=1.00 loops=1)
Filter: (a = list_part_fn(a))
(9 rows)
-- Ensure pruning does not take place when the expression contains a Var.
explain (analyze, costs off, summary off, timing off, buffers off) select * from list_part where a = list_part_fn(1) + a;
- QUERY PLAN
-------------------------------------------------------------------
- Append (actual rows=0 loops=1)
- -> Seq Scan on list_part1 list_part_1 (actual rows=0 loops=1)
+ QUERY PLAN
+---------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
+ -> Seq Scan on list_part1 list_part_1 (actual rows=0.00 loops=1)
Filter: (a = (list_part_fn(1) + a))
Rows Removed by Filter: 1
- -> Seq Scan on list_part2 list_part_2 (actual rows=0 loops=1)
+ -> Seq Scan on list_part2 list_part_2 (actual rows=0.00 loops=1)
Filter: (a = (list_part_fn(1) + a))
Rows Removed by Filter: 1
- -> Seq Scan on list_part3 list_part_3 (actual rows=0 loops=1)
+ -> Seq Scan on list_part3 list_part_3 (actual rows=0.00 loops=1)
Filter: (a = (list_part_fn(1) + a))
Rows Removed by Filter: 1
- -> Seq Scan on list_part4 list_part_4 (actual rows=0 loops=1)
+ -> Seq Scan on list_part4 list_part_4 (actual rows=0.00 loops=1)
Filter: (a = (list_part_fn(1) + a))
Rows Removed by Filter: 1
(13 rows)
@@ -2672,15 +2672,15 @@ reset max_parallel_workers_per_gather;
-- Test run-time partition pruning with an initplan
explain (analyze, costs off, summary off, timing off, buffers off)
select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1 from lprt_a);
- QUERY PLAN
--------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
InitPlan 1
- -> Aggregate (actual rows=1 loops=1)
- -> Seq Scan on lprt_a (actual rows=102 loops=1)
+ -> Aggregate (actual rows=1.00 loops=1)
+ -> Seq Scan on lprt_a (actual rows=102.00 loops=1)
InitPlan 2
- -> Aggregate (actual rows=1 loops=1)
- -> Seq Scan on lprt_a lprt_a_1 (actual rows=102 loops=1)
+ -> Aggregate (actual rows=1.00 loops=1)
+ -> Seq Scan on lprt_a lprt_a_1 (actual rows=102.00 loops=1)
-> Bitmap Heap Scan on ab_a1_b1 ab_1 (never executed)
Recheck Cond: (a = (InitPlan 1).col1)
Filter: (b = (InitPlan 2).col1)
@@ -2716,10 +2716,10 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
Filter: (b = (InitPlan 2).col1)
-> Bitmap Index Scan on ab_a3_b1_a_idx (never executed)
Index Cond: (a = (InitPlan 1).col1)
- -> Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0 loops=1)
+ -> Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0.00 loops=1)
Recheck Cond: (a = (InitPlan 1).col1)
Filter: (b = (InitPlan 2).col1)
- -> Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = (InitPlan 1).col1)
-> Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed)
Recheck Cond: (a = (InitPlan 1).col1)
@@ -2731,16 +2731,16 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
-- Test run-time partition pruning with UNION ALL parents
explain (analyze, costs off, summary off, timing off, buffers off)
select * from (select * from ab where a = 1 union all select * from ab) ab where b = (select 1);
- QUERY PLAN
--------------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
InitPlan 1
- -> Result (actual rows=1 loops=1)
- -> Append (actual rows=0 loops=1)
- -> Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0 loops=1)
+ -> Result (actual rows=1.00 loops=1)
+ -> Append (actual rows=0.00 loops=1)
+ -> Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0.00 loops=1)
Recheck Cond: (a = 1)
Filter: (b = (InitPlan 1).col1)
- -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = 1)
-> Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
Recheck Cond: (a = 1)
@@ -2752,19 +2752,19 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
Filter: (b = (InitPlan 1).col1)
-> Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
Index Cond: (a = 1)
- -> Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a1_b2 ab_2 (never executed)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a1_b3 ab_3 (never executed)
Filter: (b = (InitPlan 1).col1)
- -> Seq Scan on ab_a2_b1 ab_4 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a2_b2 ab_5 (never executed)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a2_b3 ab_6 (never executed)
Filter: (b = (InitPlan 1).col1)
- -> Seq Scan on ab_a3_b1 ab_7 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a3_b2 ab_8 (never executed)
Filter: (b = (InitPlan 1).col1)
@@ -2775,16 +2775,16 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
-- A case containing a UNION ALL with a non-partitioned child.
explain (analyze, costs off, summary off, timing off, buffers off)
select * from (select * from ab where a = 1 union all (values(10,5)) union all select * from ab) ab where b = (select 1);
- QUERY PLAN
--------------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
InitPlan 1
- -> Result (actual rows=1 loops=1)
- -> Append (actual rows=0 loops=1)
- -> Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0 loops=1)
+ -> Result (actual rows=1.00 loops=1)
+ -> Append (actual rows=0.00 loops=1)
+ -> Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0.00 loops=1)
Recheck Cond: (a = 1)
Filter: (b = (InitPlan 1).col1)
- -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = 1)
-> Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
Recheck Cond: (a = 1)
@@ -2796,21 +2796,21 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
Filter: (b = (InitPlan 1).col1)
-> Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
Index Cond: (a = 1)
- -> Result (actual rows=0 loops=1)
+ -> Result (actual rows=0.00 loops=1)
One-Time Filter: (5 = (InitPlan 1).col1)
- -> Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a1_b2 ab_2 (never executed)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a1_b3 ab_3 (never executed)
Filter: (b = (InitPlan 1).col1)
- -> Seq Scan on ab_a2_b1 ab_4 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a2_b2 ab_5 (never executed)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a2_b3 ab_6 (never executed)
Filter: (b = (InitPlan 1).col1)
- -> Seq Scan on ab_a3_b1 ab_7 (actual rows=0 loops=1)
+ -> Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a3_b2 ab_8 (never executed)
Filter: (b = (InitPlan 1).col1)
@@ -2835,17 +2835,17 @@ union all
explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q6(1);
QUERY PLAN
--------------------------------------------------------
- Append (actual rows=0 loops=1)
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 12
InitPlan 1
- -> Result (actual rows=1 loops=1)
+ -> Result (actual rows=1.00 loops=1)
-> Seq Scan on ab_a1_b1 ab_1 (never executed)
Filter: ((a = $1) AND (b = (InitPlan 1).col1))
-> Seq Scan on ab_a1_b2 ab_2 (never executed)
Filter: ((a = $1) AND (b = (InitPlan 1).col1))
-> Seq Scan on ab_a1_b3 ab_3 (never executed)
Filter: ((a = $1) AND (b = (InitPlan 1).col1))
- -> Seq Scan on xy_1 (actual rows=0 loops=1)
+ -> Seq Scan on xy_1 (actual rows=0.00 loops=1)
Filter: ((x = $1) AND (y = (InitPlan 1).col1))
Rows Removed by Filter: 1
-> Seq Scan on ab_a1_b1 ab_4 (never executed)
@@ -2875,44 +2875,44 @@ deallocate ab_q6;
insert into ab values (1,2);
select explain_analyze('
update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;');
- explain_analyze
--------------------------------------------------------------------------------------------
- Update on ab_a1 (actual rows=0 loops=1)
+ explain_analyze
+----------------------------------------------------------------------------------------------
+ Update on ab_a1 (actual rows=0.00 loops=1)
Update on ab_a1_b1 ab_a1_1
Update on ab_a1_b2 ab_a1_2
Update on ab_a1_b3 ab_a1_3
- -> Nested Loop (actual rows=1 loops=1)
- -> Append (actual rows=1 loops=1)
- -> Bitmap Heap Scan on ab_a1_b1 ab_a1_1 (actual rows=0 loops=1)
+ -> Nested Loop (actual rows=1.00 loops=1)
+ -> Append (actual rows=1.00 loops=1)
+ -> Bitmap Heap Scan on ab_a1_b1 ab_a1_1 (actual rows=0.00 loops=1)
Recheck Cond: (a = 1)
- -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = 1)
- -> Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
+ -> Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1.00 loops=1)
Recheck Cond: (a = 1)
Heap Blocks: exact=1
- -> Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
+ -> Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1.00 loops=1)
Index Cond: (a = 1)
- -> Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
+ -> Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0.00 loops=1)
Recheck Cond: (a = 1)
Heap Blocks: exact=1
- -> Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
+ -> Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1.00 loops=1)
Index Cond: (a = 1)
- -> Materialize (actual rows=1 loops=1)
+ -> Materialize (actual rows=1.00 loops=1)
Storage: Memory Maximum Storage: NkB
- -> Append (actual rows=1 loops=1)
- -> Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+ -> Append (actual rows=1.00 loops=1)
+ -> Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1)
Recheck Cond: (a = 1)
- -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
+ -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1)
Index Cond: (a = 1)
- -> Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
+ -> Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1.00 loops=1)
Recheck Cond: (a = 1)
Heap Blocks: exact=1
- -> Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
+ -> Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1.00 loops=1)
Index Cond: (a = 1)
- -> Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
+ -> Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0.00 loops=1)
Recheck Cond: (a = 1)
Heap Blocks: exact=1
- -> Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
+ -> Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1.00 loops=1)
Index Cond: (a = 1)
(37 rows)
@@ -2927,23 +2927,23 @@ truncate ab;
insert into ab values (1, 1), (1, 2), (1, 3), (2, 1);
select explain_analyze('
update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);');
- explain_analyze
-------------------------------------------------------------------------------
- Update on ab_a1 (actual rows=0 loops=1)
+ explain_analyze
+---------------------------------------------------------------------------------
+ Update on ab_a1 (actual rows=0.00 loops=1)
Update on ab_a1_b1 ab_a1_1
Update on ab_a1_b2 ab_a1_2
Update on ab_a1_b3 ab_a1_3
InitPlan 1
- -> Result (actual rows=1 loops=1)
- -> Nested Loop (actual rows=3 loops=1)
- -> Append (actual rows=3 loops=1)
- -> Seq Scan on ab_a1_b1 ab_a1_1 (actual rows=1 loops=1)
- -> Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
- -> Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=1 loops=1)
+ -> Result (actual rows=1.00 loops=1)
+ -> Nested Loop (actual rows=3.00 loops=1)
+ -> Append (actual rows=3.00 loops=1)
+ -> Seq Scan on ab_a1_b1 ab_a1_1 (actual rows=1.00 loops=1)
+ -> Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1.00 loops=1)
+ -> Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=1.00 loops=1)
-> Materialize (actual rows=1.00 loops=3)
Storage: Memory Maximum Storage: NkB
- -> Append (actual rows=1 loops=1)
- -> Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
+ -> Append (actual rows=1.00 loops=1)
+ -> Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1.00 loops=1)
Filter: (b = (InitPlan 1).col1)
-> Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
Filter: (b = (InitPlan 1).col1)
@@ -2985,12 +2985,12 @@ explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 join tprt on tbl1.col1 > tprt.col1;
QUERY PLAN
-----------------------------------------------------------------------------
- Nested Loop (actual rows=6 loops=1)
- -> Seq Scan on tbl1 (actual rows=2 loops=1)
+ Nested Loop (actual rows=6.00 loops=1)
+ -> Seq Scan on tbl1 (actual rows=2.00 loops=1)
-> Append (actual rows=3.00 loops=2)
-> Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=2)
Index Cond: (col1 < tbl1.col1)
- -> Index Scan using tprt2_idx on tprt_2 (actual rows=2 loops=1)
+ -> Index Scan using tprt2_idx on tprt_2 (actual rows=2.00 loops=1)
Index Cond: (col1 < tbl1.col1)
-> Index Scan using tprt3_idx on tprt_3 (never executed)
Index Cond: (col1 < tbl1.col1)
@@ -3006,8 +3006,8 @@ explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
QUERY PLAN
-----------------------------------------------------------------------------
- Nested Loop (actual rows=2 loops=1)
- -> Seq Scan on tbl1 (actual rows=2 loops=1)
+ Nested Loop (actual rows=2.00 loops=1)
+ -> Seq Scan on tbl1 (actual rows=2.00 loops=1)
-> Append (actual rows=1.00 loops=2)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
@@ -3051,8 +3051,8 @@ explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 > tprt.col1;
QUERY PLAN
-----------------------------------------------------------------------------
- Nested Loop (actual rows=23 loops=1)
- -> Seq Scan on tbl1 (actual rows=5 loops=1)
+ Nested Loop (actual rows=23.00 loops=1)
+ -> Seq Scan on tbl1 (actual rows=5.00 loops=1)
-> Append (actual rows=4.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (actual rows=2.00 loops=5)
Index Cond: (col1 < tbl1.col1)
@@ -3072,8 +3072,8 @@ explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 inner join tprt on tbl1.col1 = tprt.col1;
QUERY PLAN
-----------------------------------------------------------------------------
- Nested Loop (actual rows=3 loops=1)
- -> Seq Scan on tbl1 (actual rows=5 loops=1)
+ Nested Loop (actual rows=3.00 loops=1)
+ -> Seq Scan on tbl1 (actual rows=5.00 loops=1)
-> Append (actual rows=0.60 loops=5)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
@@ -3134,11 +3134,11 @@ delete from tbl1;
insert into tbl1 values (4400);
explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
- QUERY PLAN
---------------------------------------------------------------------------
- Nested Loop (actual rows=1 loops=1)
- -> Seq Scan on tbl1 (actual rows=1 loops=1)
- -> Append (actual rows=1 loops=1)
+ QUERY PLAN
+-----------------------------------------------------------------------------
+ Nested Loop (actual rows=1.00 loops=1)
+ -> Seq Scan on tbl1 (actual rows=1.00 loops=1)
+ -> Append (actual rows=1.00 loops=1)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 > tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (never executed)
@@ -3149,7 +3149,7 @@ select * from tbl1 join tprt on tbl1.col1 < tprt.col1;
Index Cond: (col1 > tbl1.col1)
-> Index Scan using tprt5_idx on tprt_5 (never executed)
Index Cond: (col1 > tbl1.col1)
- -> Index Scan using tprt6_idx on tprt_6 (actual rows=1 loops=1)
+ -> Index Scan using tprt6_idx on tprt_6 (actual rows=1.00 loops=1)
Index Cond: (col1 > tbl1.col1)
(15 rows)
@@ -3168,9 +3168,9 @@ explain (analyze, costs off, summary off, timing off, buffers off)
select * from tbl1 join tprt on tbl1.col1 = tprt.col1;
QUERY PLAN
-------------------------------------------------------------------
- Nested Loop (actual rows=0 loops=1)
- -> Seq Scan on tbl1 (actual rows=1 loops=1)
- -> Append (actual rows=0 loops=1)
+ Nested Loop (actual rows=0.00 loops=1)
+ -> Seq Scan on tbl1 (actual rows=1.00 loops=1)
+ -> Append (actual rows=0.00 loops=1)
-> Index Scan using tprt1_idx on tprt_1 (never executed)
Index Cond: (col1 = tbl1.col1)
-> Index Scan using tprt2_idx on tprt_2 (never executed)
@@ -3205,9 +3205,9 @@ prepare part_abc_q1 (int, int, int) as
select * from part_abc where a = $1 and b = $2 and c = $3;
-- Single partition should be scanned.
explain (analyze, costs off, summary off, timing off, buffers off) execute part_abc_q1 (1, 2, 3);
- QUERY PLAN
-----------------------------------------------------------
- Seq Scan on part_abc_p1 part_abc (actual rows=0 loops=1)
+ QUERY PLAN
+-------------------------------------------------------------
+ Seq Scan on part_abc_p1 part_abc (actual rows=0.00 loops=1)
Filter: ((a = $1) AND (b = $2) AND (c = $3))
(2 rows)
@@ -3230,28 +3230,28 @@ select * from listp where b = 1;
-- which match the given parameter.
prepare q1 (int,int) as select * from listp where b in ($1,$2);
explain (analyze, costs off, summary off, timing off, buffers off) execute q1 (1,1);
- QUERY PLAN
--------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 1
- -> Seq Scan on listp_1_1 listp_1 (actual rows=0 loops=1)
+ -> Seq Scan on listp_1_1 listp_1 (actual rows=0.00 loops=1)
Filter: (b = ANY (ARRAY[$1, $2]))
(4 rows)
explain (analyze, costs off, summary off, timing off, buffers off) execute q1 (2,2);
- QUERY PLAN
--------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+----------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 1
- -> Seq Scan on listp_2_1 listp_1 (actual rows=0 loops=1)
+ -> Seq Scan on listp_2_1 listp_1 (actual rows=0.00 loops=1)
Filter: (b = ANY (ARRAY[$1, $2]))
(4 rows)
-- Try with no matching partitions.
explain (analyze, costs off, summary off, timing off, buffers off) execute q1 (0,0);
- QUERY PLAN
---------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 2
(2 rows)
@@ -3262,17 +3262,17 @@ prepare q1 (int,int,int,int) as select * from listp where b in($1,$2) and $3 <>
explain (analyze, costs off, summary off, timing off, buffers off) execute q1 (1,2,2,0);
QUERY PLAN
-------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 1
- -> Seq Scan on listp_1_1 listp_1 (actual rows=0 loops=1)
+ -> Seq Scan on listp_1_1 listp_1 (actual rows=0.00 loops=1)
Filter: ((b = ANY (ARRAY[$1, $2])) AND ($3 <> b) AND ($4 <> b))
(4 rows)
-- Both partitions allowed by IN clause, then both excluded again by <> clauses.
explain (analyze, costs off, summary off, timing off, buffers off) execute q1 (1,2,2,1);
- QUERY PLAN
---------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 2
(2 rows)
@@ -3281,9 +3281,9 @@ explain (analyze, costs off, summary off, timing off, buffers off)
select * from listp where a = (select null::int);
QUERY PLAN
------------------------------------------------------
- Append (actual rows=0 loops=1)
+ Append (actual rows=0.00 loops=1)
InitPlan 1
- -> Result (actual rows=1 loops=1)
+ -> Result (actual rows=1.00 loops=1)
-> Seq Scan on listp_1_1 listp_1 (never executed)
Filter: (a = (InitPlan 1).col1)
-> Seq Scan on listp_2_1 listp_2 (never executed)
@@ -3304,24 +3304,24 @@ create table stable_qual_pruning3 partition of stable_qual_pruning
-- comparison against a stable value requires run-time pruning
explain (analyze, costs off, summary off, timing off, buffers off)
select * from stable_qual_pruning where a < localtimestamp;
- QUERY PLAN
---------------------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 1
- -> Seq Scan on stable_qual_pruning1 stable_qual_pruning_1 (actual rows=0 loops=1)
+ -> Seq Scan on stable_qual_pruning1 stable_qual_pruning_1 (actual rows=0.00 loops=1)
Filter: (a < LOCALTIMESTAMP)
- -> Seq Scan on stable_qual_pruning2 stable_qual_pruning_2 (actual rows=0 loops=1)
+ -> Seq Scan on stable_qual_pruning2 stable_qual_pruning_2 (actual rows=0.00 loops=1)
Filter: (a < LOCALTIMESTAMP)
(6 rows)
-- timestamp < timestamptz comparison is only stable, not immutable
explain (analyze, costs off, summary off, timing off, buffers off)
select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
- QUERY PLAN
---------------------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 2
- -> Seq Scan on stable_qual_pruning1 stable_qual_pruning_1 (actual rows=0 loops=1)
+ -> Seq Scan on stable_qual_pruning1 stable_qual_pruning_1 (actual rows=0.00 loops=1)
Filter: (a < 'Tue Feb 01 00:00:00 2000 PST'::timestamp with time zone)
(4 rows)
@@ -3329,9 +3329,9 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz;
explain (analyze, costs off, summary off, timing off, buffers off)
select * from stable_qual_pruning
where a = any(array['2010-02-01', '2020-01-01']::timestamp[]);
- QUERY PLAN
---------------------------------
- Result (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------
+ Result (actual rows=0.00 loops=1)
One-Time Filter: false
(2 rows)
@@ -3340,7 +3340,7 @@ select * from stable_qual_pruning
where a = any(array['2000-02-01', '2010-01-01']::timestamp[]);
QUERY PLAN
----------------------------------------------------------------------------------------------------------------
- Seq Scan on stable_qual_pruning2 stable_qual_pruning (actual rows=0 loops=1)
+ Seq Scan on stable_qual_pruning2 stable_qual_pruning (actual rows=0.00 loops=1)
Filter: (a = ANY ('{"Tue Feb 01 00:00:00 2000","Fri Jan 01 00:00:00 2010"}'::timestamp without time zone[]))
(2 rows)
@@ -3349,18 +3349,18 @@ select * from stable_qual_pruning
where a = any(array['2000-02-01', localtimestamp]::timestamp[]);
QUERY PLAN
------------------------------------------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 2
- -> Seq Scan on stable_qual_pruning2 stable_qual_pruning_1 (actual rows=0 loops=1)
+ -> Seq Scan on stable_qual_pruning2 stable_qual_pruning_1 (actual rows=0.00 loops=1)
Filter: (a = ANY (ARRAY['Tue Feb 01 00:00:00 2000'::timestamp without time zone, LOCALTIMESTAMP]))
(4 rows)
explain (analyze, costs off, summary off, timing off, buffers off)
select * from stable_qual_pruning
where a = any(array['2010-02-01', '2020-01-01']::timestamptz[]);
- QUERY PLAN
---------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 3
(2 rows)
@@ -3369,23 +3369,23 @@ select * from stable_qual_pruning
where a = any(array['2000-02-01', '2010-01-01']::timestamptz[]);
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ Append (actual rows=0.00 loops=1)
Subplans Removed: 2
- -> Seq Scan on stable_qual_pruning2 stable_qual_pruning_1 (actual rows=0 loops=1)
+ -> Seq Scan on stable_qual_pruning2 stable_qual_pruning_1 (actual rows=0.00 loops=1)
Filter: (a = ANY ('{"Tue Feb 01 00:00:00 2000 PST","Fri Jan 01 00:00:00 2010 PST"}'::timestamp with time zone[]))
(4 rows)
explain (analyze, costs off, summary off, timing off, buffers off)
select * from stable_qual_pruning
where a = any(null::timestamptz[]);
- QUERY PLAN
---------------------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
- -> Seq Scan on stable_qual_pruning1 stable_qual_pruning_1 (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
+ -> Seq Scan on stable_qual_pruning1 stable_qual_pruning_1 (actual rows=0.00 loops=1)
Filter: (a = ANY (NULL::timestamp with time zone[]))
- -> Seq Scan on stable_qual_pruning2 stable_qual_pruning_2 (actual rows=0 loops=1)
+ -> Seq Scan on stable_qual_pruning2 stable_qual_pruning_2 (actual rows=0.00 loops=1)
Filter: (a = ANY (NULL::timestamp with time zone[]))
- -> Seq Scan on stable_qual_pruning3 stable_qual_pruning_3 (actual rows=0 loops=1)
+ -> Seq Scan on stable_qual_pruning3 stable_qual_pruning_3 (actual rows=0.00 loops=1)
Filter: (a = ANY (NULL::timestamp with time zone[]))
(7 rows)
@@ -3405,14 +3405,14 @@ create table mc3p2 partition of mc3p
insert into mc3p values (0, 1, 1), (1, 1, 1), (2, 1, 1);
explain (analyze, costs off, summary off, timing off, buffers off)
select * from mc3p where a < 3 and abs(b) = 1;
- QUERY PLAN
---------------------------------------------------------
- Append (actual rows=3 loops=1)
- -> Seq Scan on mc3p0 mc3p_1 (actual rows=1 loops=1)
+ QUERY PLAN
+-----------------------------------------------------------
+ Append (actual rows=3.00 loops=1)
+ -> Seq Scan on mc3p0 mc3p_1 (actual rows=1.00 loops=1)
Filter: ((a < 3) AND (abs(b) = 1))
- -> Seq Scan on mc3p1 mc3p_2 (actual rows=1 loops=1)
+ -> Seq Scan on mc3p1 mc3p_2 (actual rows=1.00 loops=1)
Filter: ((a < 3) AND (abs(b) = 1))
- -> Seq Scan on mc3p2 mc3p_3 (actual rows=1 loops=1)
+ -> Seq Scan on mc3p2 mc3p_3 (actual rows=1.00 loops=1)
Filter: ((a < 3) AND (abs(b) = 1))
(7 rows)
@@ -3427,11 +3427,11 @@ explain (analyze, costs off, summary off, timing off, buffers off)
execute ps1(1);
QUERY PLAN
-------------------------------------------------------------
- Append (actual rows=1 loops=1)
+ Append (actual rows=1.00 loops=1)
Subplans Removed: 2
InitPlan 1
- -> Result (actual rows=1 loops=1)
- -> Seq Scan on mc3p1 mc3p_1 (actual rows=1 loops=1)
+ -> Result (actual rows=1.00 loops=1)
+ -> Seq Scan on mc3p1 mc3p_1 (actual rows=1.00 loops=1)
Filter: ((a = $1) AND (abs(b) < (InitPlan 1).col1))
(6 rows)
@@ -3442,13 +3442,13 @@ explain (analyze, costs off, summary off, timing off, buffers off)
execute ps2(1);
QUERY PLAN
--------------------------------------------------------------
- Append (actual rows=2 loops=1)
+ Append (actual rows=2.00 loops=1)
Subplans Removed: 1
InitPlan 1
- -> Result (actual rows=1 loops=1)
- -> Seq Scan on mc3p0 mc3p_1 (actual rows=1 loops=1)
+ -> Result (actual rows=1.00 loops=1)
+ -> Seq Scan on mc3p0 mc3p_1 (actual rows=1.00 loops=1)
Filter: ((a <= $1) AND (abs(b) < (InitPlan 1).col1))
- -> Seq Scan on mc3p1 mc3p_2 (actual rows=1 loops=1)
+ -> Seq Scan on mc3p1 mc3p_2 (actual rows=1.00 loops=1)
Filter: ((a <= $1) AND (abs(b) < (InitPlan 1).col1))
(8 rows)
@@ -3462,29 +3462,29 @@ create table boolp_t partition of boolp for values in('t');
create table boolp_f partition of boolp for values in('f');
explain (analyze, costs off, summary off, timing off, buffers off)
select * from boolp where a = (select value from boolvalues where value);
- QUERY PLAN
------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
InitPlan 1
- -> Seq Scan on boolvalues (actual rows=1 loops=1)
+ -> Seq Scan on boolvalues (actual rows=1.00 loops=1)
Filter: value
Rows Removed by Filter: 1
-> Seq Scan on boolp_f boolp_1 (never executed)
Filter: (a = (InitPlan 1).col1)
- -> Seq Scan on boolp_t boolp_2 (actual rows=0 loops=1)
+ -> Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
Filter: (a = (InitPlan 1).col1)
(9 rows)
explain (analyze, costs off, summary off, timing off, buffers off)
select * from boolp where a = (select value from boolvalues where not value);
- QUERY PLAN
------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
InitPlan 1
- -> Seq Scan on boolvalues (actual rows=1 loops=1)
+ -> Seq Scan on boolvalues (actual rows=1.00 loops=1)
Filter: (NOT value)
Rows Removed by Filter: 1
- -> Seq Scan on boolp_f boolp_1 (actual rows=0 loops=1)
+ -> Seq Scan on boolp_f boolp_1 (actual rows=0.00 loops=1)
Filter: (a = (InitPlan 1).col1)
-> Seq Scan on boolp_t boolp_2 (never executed)
Filter: (a = (InitPlan 1).col1)
@@ -3505,15 +3505,15 @@ create index on ma_test (b);
analyze ma_test;
prepare mt_q1 (int) as select a from ma_test where a >= $1 and a % 10 = 5 order by b;
explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1(15);
- QUERY PLAN
------------------------------------------------------------------------------------------
- Merge Append (actual rows=2 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------------------------------
+ Merge Append (actual rows=2.00 loops=1)
Sort Key: ma_test.b
Subplans Removed: 1
- -> Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1 loops=1)
+ -> Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1.00 loops=1)
Filter: ((a >= $1) AND ((a % 10) = 5))
Rows Removed by Filter: 9
- -> Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1 loops=1)
+ -> Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1.00 loops=1)
Filter: ((a >= $1) AND ((a % 10) = 5))
Rows Removed by Filter: 9
(9 rows)
@@ -3526,12 +3526,12 @@ execute mt_q1(15);
(2 rows)
explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1(25);
- QUERY PLAN
------------------------------------------------------------------------------------------
- Merge Append (actual rows=1 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------------------------------
+ Merge Append (actual rows=1.00 loops=1)
Sort Key: ma_test.b
Subplans Removed: 2
- -> Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1 loops=1)
+ -> Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1.00 loops=1)
Filter: ((a >= $1) AND ((a % 10) = 5))
Rows Removed by Filter: 9
(6 rows)
@@ -3544,9 +3544,9 @@ execute mt_q1(25);
-- Ensure MergeAppend behaves correctly when no subplans match
explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1(35);
- QUERY PLAN
---------------------------------------
- Merge Append (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------------
+ Merge Append (actual rows=0.00 loops=1)
Sort Key: ma_test.b
Subplans Removed: 3
(3 rows)
@@ -3560,11 +3560,11 @@ deallocate mt_q1;
prepare mt_q2 (int) as select * from ma_test where a >= $1 order by b limit 1;
-- Ensure output list looks sane when the MergeAppend has no subplans.
explain (analyze, verbose, costs off, summary off, timing off, buffers off) execute mt_q2 (35);
- QUERY PLAN
---------------------------------------------
- Limit (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------------------
+ Limit (actual rows=0.00 loops=1)
Output: ma_test.a, ma_test.b
- -> Merge Append (actual rows=0 loops=1)
+ -> Merge Append (actual rows=0.00 loops=1)
Sort Key: ma_test.b
Subplans Removed: 3
(5 rows)
@@ -3572,21 +3572,21 @@ explain (analyze, verbose, costs off, summary off, timing off, buffers off) exec
deallocate mt_q2;
-- ensure initplan params properly prune partitions
explain (analyze, costs off, summary off, timing off, buffers off) select * from ma_test where a >= (select min(b) from ma_test_p2) order by b;
- QUERY PLAN
------------------------------------------------------------------------------------------------
- Merge Append (actual rows=20 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------
+ Merge Append (actual rows=20.00 loops=1)
Sort Key: ma_test.b
InitPlan 2
- -> Result (actual rows=1 loops=1)
+ -> Result (actual rows=1.00 loops=1)
InitPlan 1
- -> Limit (actual rows=1 loops=1)
- -> Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1 loops=1)
+ -> Limit (actual rows=1.00 loops=1)
+ -> Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1.00 loops=1)
Index Cond: (b IS NOT NULL)
-> Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed)
Filter: (a >= (InitPlan 2).col1)
- -> Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10 loops=1)
+ -> Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10.00 loops=1)
Filter: (a >= (InitPlan 2).col1)
- -> Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10 loops=1)
+ -> Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10.00 loops=1)
Filter: (a >= (InitPlan 2).col1)
(14 rows)
@@ -4023,9 +4023,9 @@ create table listp2 partition of listp for values in(2) partition by list(b);
create table listp2_10 partition of listp2 for values in (10);
explain (analyze, costs off, summary off, timing off, buffers off)
select * from listp where a = (select 2) and b <> 10;
- QUERY PLAN
----------------------------------------------------
- Seq Scan on listp1 listp (actual rows=0 loops=1)
+ QUERY PLAN
+-----------------------------------------------------
+ Seq Scan on listp1 listp (actual rows=0.00 loops=1)
Filter: ((b <> 10) AND (a = (InitPlan 1).col1))
InitPlan 1
-> Result (never executed)
@@ -4148,22 +4148,22 @@ create index on rangep (a);
-- Ensure run-time pruning works on the nested Merge Append
explain (analyze on, costs off, timing off, summary off, buffers off)
select * from rangep where b IN((select 1),(select 2)) order by a;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------
- Append (actual rows=0 loops=1)
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------
+ Append (actual rows=0.00 loops=1)
InitPlan 1
- -> Result (actual rows=1 loops=1)
+ -> Result (actual rows=1.00 loops=1)
InitPlan 2
- -> Result (actual rows=1 loops=1)
- -> Merge Append (actual rows=0 loops=1)
+ -> Result (actual rows=1.00 loops=1)
+ -> Merge Append (actual rows=0.00 loops=1)
Sort Key: rangep_2.a
- -> Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0 loops=1)
+ -> Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0.00 loops=1)
Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
- -> Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0 loops=1)
+ -> Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0.00 loops=1)
Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
-> Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed)
Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
- -> Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0 loops=1)
+ -> Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0.00 loops=1)
Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1]))
(15 rows)
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 88911ca2b9f..cd79abc35db 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -759,9 +759,9 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
-- actually run the query with an analyze to use the partial index
explain (costs off, analyze on, timing off, summary off, buffers off)
select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
- QUERY PLAN
------------------------------------------------------------------
- Index Scan using onek2_u2_prtl on onek2 (actual rows=1 loops=1)
+ QUERY PLAN
+--------------------------------------------------------------------
+ Index Scan using onek2_u2_prtl on onek2 (actual rows=1.00 loops=1)
Index Cond: (unique2 = 11)
Filter: (stringu1 = 'ATAAAA'::name)
(3 rows)
diff --git a/src/test/regress/expected/select_into.out b/src/test/regress/expected/select_into.out
index 5a45eac5ff9..d04ca2b1bf7 100644
--- a/src/test/regress/expected/select_into.out
+++ b/src/test/regress/expected/select_into.out
@@ -28,10 +28,10 @@ ERROR: permission denied for table tbl_withdata1
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
CREATE TABLE selinto_schema.tbl_withdata2 (a) AS
SELECT generate_series(1,3) WITH DATA;
- QUERY PLAN
---------------------------------------
- ProjectSet (actual rows=3 loops=1)
- -> Result (actual rows=1 loops=1)
+ QUERY PLAN
+-----------------------------------------
+ ProjectSet (actual rows=3.00 loops=1)
+ -> Result (actual rows=1.00 loops=1)
(2 rows)
-- WITH NO DATA, passes.
@@ -53,10 +53,10 @@ CREATE TABLE selinto_schema.tbl_withdata3 (a) AS
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
CREATE TABLE selinto_schema.tbl_withdata4 (a) AS
EXECUTE data_sel WITH DATA;
- QUERY PLAN
---------------------------------------
- ProjectSet (actual rows=3 loops=1)
- -> Result (actual rows=1 loops=1)
+ QUERY PLAN
+-----------------------------------------
+ ProjectSet (actual rows=3.00 loops=1)
+ -> Result (actual rows=1.00 loops=1)
(2 rows)
-- EXECUTE and WITH NO DATA, passes.
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 4732ad6bfb4..56509540f2a 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -585,9 +585,9 @@ explain (analyze, timing off, summary off, costs off, buffers off)
and tenk2.thousand=0;
QUERY PLAN
-----------------------------------------------------------------------------
- Aggregate (actual rows=1 loops=1)
- -> Nested Loop (actual rows=98000 loops=1)
- -> Seq Scan on tenk2 (actual rows=10 loops=1)
+ Aggregate (actual rows=1.00 loops=1)
+ -> Nested Loop (actual rows=98000.00 loops=1)
+ -> Seq Scan on tenk2 (actual rows=10.00 loops=1)
Filter: (thousand = 0)
Rows Removed by Filter: 9990
-> Gather (actual rows=9800.00 loops=10)
@@ -619,8 +619,8 @@ $$;
select * from explain_parallel_sort_stats();
explain_parallel_sort_stats
-----------------------------------------------------------------------------
- Nested Loop Left Join (actual rows=30000 loops=1)
- -> Values Scan on "*VALUES*" (actual rows=3 loops=1)
+ Nested Loop Left Join (actual rows=30000.00 loops=1)
+ -> Values Scan on "*VALUES*" (actual rows=3.00 loops=1)
-> Gather Merge (actual rows=10000.00 loops=3)
Workers Planned: 4
Workers Launched: 4
@@ -1172,7 +1172,7 @@ SET LOCAL debug_parallel_query = 1;
EXPLAIN (analyze, timing off, summary off, costs off, buffers off) SELECT * FROM tenk1;
QUERY PLAN
----------------------------------------------------------------
- Gather (actual rows=10000 loops=1)
+ Gather (actual rows=10000.00 loops=1)
Workers Planned: 4
Workers Launched: 4
-> Parallel Seq Scan on tenk1 (actual rows=2000.00 loops=5)
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index ebc545e2461..d0db8a412ff 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -1726,14 +1726,14 @@ begin
end;
$$;
select * from explain_sq_limit();
- explain_sq_limit
-----------------------------------------------------------------
- Limit (actual rows=3 loops=1)
- -> Subquery Scan on x (actual rows=3 loops=1)
- -> Sort (actual rows=3 loops=1)
+ explain_sq_limit
+-------------------------------------------------------------------
+ Limit (actual rows=3.00 loops=1)
+ -> Subquery Scan on x (actual rows=3.00 loops=1)
+ -> Sort (actual rows=3.00 loops=1)
Sort Key: sq_limit.c1, sq_limit.pk
Sort Method: top-N heapsort Memory: xxx
- -> Seq Scan on sq_limit (actual rows=8 loops=1)
+ -> Seq Scan on sq_limit (actual rows=8.00 loops=1)
(6 rows)
select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3;
diff --git a/src/test/regress/expected/tidscan.out b/src/test/regress/expected/tidscan.out
index f6ebdf0601f..e823bc91c57 100644
--- a/src/test/regress/expected/tidscan.out
+++ b/src/test/regress/expected/tidscan.out
@@ -191,10 +191,10 @@ FETCH NEXT FROM c;
-- perform update
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
UPDATE tidscan SET id = -id WHERE CURRENT OF c RETURNING *;
- QUERY PLAN
----------------------------------------------------
- Update on tidscan (actual rows=1 loops=1)
- -> Tid Scan on tidscan (actual rows=1 loops=1)
+ QUERY PLAN
+------------------------------------------------------
+ Update on tidscan (actual rows=1.00 loops=1)
+ -> Tid Scan on tidscan (actual rows=1.00 loops=1)
TID Cond: CURRENT OF c
(3 rows)
@@ -207,10 +207,10 @@ FETCH NEXT FROM c;
-- perform update
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
UPDATE tidscan SET id = -id WHERE CURRENT OF c RETURNING *;
- QUERY PLAN
----------------------------------------------------
- Update on tidscan (actual rows=1 loops=1)
- -> Tid Scan on tidscan (actual rows=1 loops=1)
+ QUERY PLAN
+------------------------------------------------------
+ Update on tidscan (actual rows=1.00 loops=1)
+ -> Tid Scan on tidscan (actual rows=1.00 loops=1)
TID Cond: CURRENT OF c
(3 rows)
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index c719cca6583..0bafa870496 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -147,9 +147,6 @@ select jsonb_pretty(
-- Also remove its sort-type fields, as those aren't 100% stable
#- '{0,Plan,Plans,0,Sort Method}'
#- '{0,Plan,Plans,0,Sort Space Type}'
- -- Actual Rows can be 0 or 0.0 depending on whether loops>1
- #- '{0,Plan,Plans,0,Actual Rows}'
- #- '{0,Plan,Plans,0,Plans,0,Actual Rows}'
);
rollback;
--
2.39.3 (Apple Git-145)
On 24.02.2025 19:20, Robert Haas wrote:
On Fri, Feb 21, 2025 at 10:00 PM Robert Haas <robertmhaas@gmail.com> wrote:
I guess we could do that, but how about just always displaying two
decimal places? I feel like you're really worried that people will be
confused if the row count is not an integer, but I'm not sure anyone
else has echoed that concern, and I am doubtful that it's really a
problem. Either people don't understand that the row count is divided
through by nloops, in which case the decimal places might actually
give them a clue that they've misunderstood, or they do understand
that, in which case the decimal places are not confusing. I feel like
we've gone to a lot of trouble to come up with complex solutions --
what was actually committed was one of the simpler things proposed --
but I am still not convinced that the simplest solution isn't for the
best.So here's a patch for that. Thoughts?
If no one is concerned about rows being a non-integer, then I support
this, as it's quite strange for the average rows to be an integer only
for me. If we go with this approach, we should also update all examples
in the documentation accordingly. I attached patch with changes in
documentation.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v1-0001-EXPLAIN-Always-use-two-fractional-digits-for-row-cou_DOC.patchtext/x-patch; charset=UTF-8; name=v1-0001-EXPLAIN-Always-use-two-fractional-digits-for-row-cou_DOC.patchDownload
From 7ce5d477ddd360ca3a5045949cc0e44db0658887 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Mon, 24 Feb 2025 20:07:09 +0300
Subject: [PATCH v1] EXPLAIN: Always use two fractional digits for row counts.
Commit ddb17e387aa28d61521227377b00f997756b8a27 attempted to avoid
confusing users by displaying digits after the decimal point only when
nloops > 1, since it's impossible to have a fraction row count after a
single iteration. However, this made the regression tests unstable since
parallal queries will have nloops>1 for all nodes below the Gather or
Gather Merge in normal cases, but if the workers don't start in time and
the leader finishes all the work, they will suddenly have nloops==1,
making it unpredictable whether the digits after the decimal point would
be displayed or not.
Various fixes are possible here. For example, it has previously been
proposed that we should try to display the digits after the decimal
point only if rows/nloops is an integer, but rows is a float so it's not
theoretically an exact quantity -- precision could be lost in extreme
cases. It has also been proposed that we should try to display the
digits after the decimal point only if we're under some sort of
construct that could potentially cause looping regardless of whether it
actually does. While such ideas are not without merit, this patch adopts
the much simpler solution of always display two decimal digits. If that
approach stands up to scrutiny from the buildfarm and human users, it
spares us the trouble of doing anything more complex; if not, we can
reassess.
This commit incidentally reverts 44cbba9a7f51a3888d5087fc94b23614ba2b81f2,
which should no longer be needed.
---
doc/src/sgml/auto-explain.sgml | 10 ++++----
doc/src/sgml/bloom.sgml | 16 ++++++-------
doc/src/sgml/jit.sgml | 8 +++----
doc/src/sgml/perform.sgml | 42 +++++++++++++++++-----------------
doc/src/sgml/planstats.sgml | 24 +++++++++----------
doc/src/sgml/ref/explain.sgml | 4 ++--
doc/src/sgml/rules.sgml | 18 +++++++--------
7 files changed, 61 insertions(+), 61 deletions(-)
diff --git a/doc/src/sgml/auto-explain.sgml b/doc/src/sgml/auto-explain.sgml
index 0c4656ee30..15c868021e 100644
--- a/doc/src/sgml/auto-explain.sgml
+++ b/doc/src/sgml/auto-explain.sgml
@@ -337,13 +337,13 @@ LOG: duration: 3.651 ms plan:
Query Text: SELECT count(*)
FROM pg_class, pg_index
WHERE oid = indrelid AND indisunique;
- Aggregate (cost=16.79..16.80 rows=1 width=0) (actual time=3.626..3.627 rows=1 loops=1)
- -> Hash Join (cost=4.17..16.55 rows=92 width=0) (actual time=3.349..3.594 rows=92 loops=1)
+ Aggregate (cost=16.79..16.80 rows=1 width=0) (actual time=3.626..3.627 rows=1.00 loops=1)
+ -> Hash Join (cost=4.17..16.55 rows=92 width=0) (actual time=3.349..3.594 rows=92.00 loops=1)
Hash Cond: (pg_class.oid = pg_index.indrelid)
- -> Seq Scan on pg_class (cost=0.00..9.55 rows=255 width=4) (actual time=0.016..0.140 rows=255 loops=1)
- -> Hash (cost=3.02..3.02 rows=92 width=4) (actual time=3.238..3.238 rows=92 loops=1)
+ -> Seq Scan on pg_class (cost=0.00..9.55 rows=255 width=4) (actual time=0.016..0.140 rows=255.00 loops=1)
+ -> Hash (cost=3.02..3.02 rows=92 width=4) (actual time=3.238..3.238 rows=92.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 4kB
- -> Seq Scan on pg_index (cost=0.00..3.02 rows=92 width=4) (actual time=0.008..3.187 rows=92 loops=1)
+ -> Seq Scan on pg_index (cost=0.00..3.02 rows=92 width=4) (actual time=0.008..3.187 rows=92.00 loops=1)
Filter: indisunique
]]></screen>
</sect2>
diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml
index 6a8a60b8c7..663a0a4a68 100644
--- a/doc/src/sgml/bloom.sgml
+++ b/doc/src/sgml/bloom.sgml
@@ -118,7 +118,7 @@ SELECT 10000000
=# EXPLAIN ANALYZE SELECT * FROM tbloom WHERE i2 = 898732 AND i5 = 123451;
QUERY PLAN
-------------------------------------------------------------------&zwsp;-----------------------------------
- Seq Scan on tbloom (cost=0.00..213744.00 rows=250 width=24) (actual time=357.059..357.059 rows=0 loops=1)
+ Seq Scan on tbloom (cost=0.00..213744.00 rows=250 width=24) (actual time=357.059..357.059 rows=0.00 loops=1)
Filter: ((i2 = 898732) AND (i5 = 123451))
Rows Removed by Filter: 10000000
Buffers: shared hit=63744
@@ -142,7 +142,7 @@ CREATE INDEX
=# EXPLAIN ANALYZE SELECT * FROM tbloom WHERE i2 = 898732 AND i5 = 123451;
QUERY PLAN
-------------------------------------------------------------------&zwsp;-----------------------------------
- Seq Scan on tbloom (cost=0.00..213744.00 rows=2 width=24) (actual time=351.016..351.017 rows=0 loops=1)
+ Seq Scan on tbloom (cost=0.00..213744.00 rows=2 width=24) (actual time=351.016..351.017 rows=0.00 loops=1)
Filter: ((i2 = 898732) AND (i5 = 123451))
Rows Removed by Filter: 10000000
Buffers: shared hit=63744
@@ -166,12 +166,12 @@ CREATE INDEX
=# EXPLAIN ANALYZE SELECT * FROM tbloom WHERE i2 = 898732 AND i5 = 123451;
QUERY PLAN
-------------------------------------------------------------------&zwsp;--------------------------------------------------
- Bitmap Heap Scan on tbloom (cost=1792.00..1799.69 rows=2 width=24) (actual time=22.605..22.606 rows=0 loops=1)
+ Bitmap Heap Scan on tbloom (cost=1792.00..1799.69 rows=2 width=24) (actual time=22.605..22.606 rows=0.00 loops=1)
Recheck Cond: ((i2 = 898732) AND (i5 = 123451))
Rows Removed by Index Recheck: 2300
Heap Blocks: exact=2256
Buffers: shared hit=21864
- -> Bitmap Index Scan on bloomidx (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300 loops=1)
+ -> Bitmap Index Scan on bloomidx (cost=0.00..178436.00 rows=1 width=0) (actual time=20.005..20.005 rows=2300.00 loops=1)
Index Cond: ((i2 = 898732) AND (i5 = 123451))
Buffers: shared hit=19608
Planning Time: 0.099 ms
@@ -201,15 +201,15 @@ CREATE INDEX
=# EXPLAIN ANALYZE SELECT * FROM tbloom WHERE i2 = 898732 AND i5 = 123451;
QUERY PLAN
-------------------------------------------------------------------&zwsp;--------------------------------------------------------
- Bitmap Heap Scan on tbloom (cost=9.29..13.30 rows=1 width=24) (actual time=0.032..0.033 rows=0 loops=1)
+ Bitmap Heap Scan on tbloom (cost=9.29..13.30 rows=1 width=24) (actual time=0.032..0.033 rows=0.00 loops=1)
Recheck Cond: ((i5 = 123451) AND (i2 = 898732))
Buffers: shared read=6
- -> BitmapAnd (cost=9.29..9.29 rows=1 width=0) (actual time=0.047..0.047 rows=0 loops=1)
+ -> BitmapAnd (cost=9.29..9.29 rows=1 width=0) (actual time=0.047..0.047 rows=0.00 loops=1)
Buffers: shared hit=6
- -> Bitmap Index Scan on btreeidx5 (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7 loops=1)
+ -> Bitmap Index Scan on btreeidx5 (cost=0.00..4.52 rows=11 width=0) (actual time=0.026..0.026 rows=7.00 loops=1)
Index Cond: (i5 = 123451)
Buffers: shared hit=3
- -> Bitmap Index Scan on btreeidx2 (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8 loops=1)
+ -> Bitmap Index Scan on btreeidx2 (cost=0.00..4.52 rows=11 width=0) (actual time=0.007..0.007 rows=8.00 loops=1)
Index Cond: (i2 = 898732)
Buffers: shared hit=3
Planning Time: 0.264 ms
diff --git a/doc/src/sgml/jit.sgml b/doc/src/sgml/jit.sgml
index 458f8acb41..44e18bf1a6 100644
--- a/doc/src/sgml/jit.sgml
+++ b/doc/src/sgml/jit.sgml
@@ -148,9 +148,9 @@
=# EXPLAIN ANALYZE SELECT SUM(relpages) FROM pg_class;
QUERY PLAN
-------------------------------------------------------------------&zwsp;------------------------------------------
- Aggregate (cost=16.27..16.29 rows=1 width=8) (actual time=0.303..0.303 rows=1 loops=1)
+ Aggregate (cost=16.27..16.29 rows=1 width=8) (actual time=0.303..0.303 rows=1.00 loops=1)
Buffers: shared hit=14
- -> Seq Scan on pg_class (cost=0.00..15.42 rows=342 width=4) (actual time=0.017..0.111 rows=356 loops=1)
+ -> Seq Scan on pg_class (cost=0.00..15.42 rows=342 width=4) (actual time=0.017..0.111 rows=356.00 loops=1)
Buffers: shared hit=14
Planning Time: 0.116 ms
Execution Time: 0.365 ms
@@ -165,9 +165,9 @@ SET
=# EXPLAIN ANALYZE SELECT SUM(relpages) FROM pg_class;
QUERY PLAN
-------------------------------------------------------------------&zwsp;------------------------------------------
- Aggregate (cost=16.27..16.29 rows=1 width=8) (actual time=6.049..6.049 rows=1 loops=1)
+ Aggregate (cost=16.27..16.29 rows=1 width=8) (actual time=6.049..6.049 rows=1.00 loops=1)
Buffers: shared hit=14
- -> Seq Scan on pg_class (cost=0.00..15.42 rows=342 width=4) (actual time=0.019..0.052 rows=356 loops=1)
+ -> Seq Scan on pg_class (cost=0.00..15.42 rows=342 width=4) (actual time=0.019..0.052 rows=356.00 loops=1)
Buffers: shared hit=14
Planning Time: 0.133 ms
JIT:
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 6a3dcc0bfc..91feb59abd 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -721,13 +721,13 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;
QUERY PLAN
-------------------------------------------------------------------&zwsp;--------------------------------------------------------------
- Nested Loop (cost=4.65..118.50 rows=10 width=488) (actual time=0.017..0.051 rows=10 loops=1)
+ Nested Loop (cost=4.65..118.50 rows=10 width=488) (actual time=0.017..0.051 rows=10.00 loops=1)
Buffers: shared hit=36 read=6
- -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10 loops=1)
+ -> Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10.00 loops=1)
Recheck Cond: (unique1 < 10)
Heap Blocks: exact=10
Buffers: shared hit=3 read=5 written=4
- -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
+ -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10.00 loops=1)
Index Cond: (unique1 < 10)
Buffers: shared hit=2
-> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1.00 loops=10)
@@ -772,23 +772,23 @@ WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;
QUERY PLAN
-------------------------------------------------------------------&zwsp;-------------------------------------------------------------------&zwsp;------
- Sort (cost=713.05..713.30 rows=100 width=488) (actual time=2.995..3.002 rows=100 loops=1)
+ Sort (cost=713.05..713.30 rows=100 width=488) (actual time=2.995..3.002 rows=100.00 loops=1)
Sort Key: t1.fivethous
Sort Method: quicksort Memory: 74kB
Buffers: shared hit=440
- -> Hash Join (cost=226.23..709.73 rows=100 width=488) (actual time=0.515..2.920 rows=100 loops=1)
+ -> Hash Join (cost=226.23..709.73 rows=100 width=488) (actual time=0.515..2.920 rows=100.00 loops=1)
Hash Cond: (t2.unique2 = t1.unique2)
Buffers: shared hit=437
- -> Seq Scan on tenk2 t2 (cost=0.00..445.00 rows=10000 width=244) (actual time=0.026..1.790 rows=10000 loops=1)
+ -> Seq Scan on tenk2 t2 (cost=0.00..445.00 rows=10000 width=244) (actual time=0.026..1.790 rows=10000.00 loops=1)
Buffers: shared hit=345
- -> Hash (cost=224.98..224.98 rows=100 width=244) (actual time=0.476..0.477 rows=100 loops=1)
+ -> Hash (cost=224.98..224.98 rows=100 width=244) (actual time=0.476..0.477 rows=100.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 35kB
Buffers: shared hit=92
- -> Bitmap Heap Scan on tenk1 t1 (cost=5.06..224.98 rows=100 width=244) (actual time=0.030..0.450 rows=100 loops=1)
+ -> Bitmap Heap Scan on tenk1 t1 (cost=5.06..224.98 rows=100 width=244) (actual time=0.030..0.450 rows=100.00 loops=1)
Recheck Cond: (unique1 < 100)
Heap Blocks: exact=90
Buffers: shared hit=92
- -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
+ -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100.00 loops=1)
Index Cond: (unique1 < 100)
Buffers: shared hit=2
Planning:
@@ -814,7 +814,7 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;
QUERY PLAN
-------------------------------------------------------------------&zwsp;--------------------------------------
- Seq Scan on tenk1 (cost=0.00..470.00 rows=7000 width=244) (actual time=0.030..1.995 rows=7000 loops=1)
+ Seq Scan on tenk1 (cost=0.00..470.00 rows=7000 width=244) (actual time=0.030..1.995 rows=7000.00 loops=1)
Filter: (ten < 7)
Rows Removed by Filter: 3000
Buffers: shared hit=345
@@ -838,7 +838,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';
QUERY PLAN
-------------------------------------------------------------------&zwsp;-----------------------------------
- Seq Scan on polygon_tbl (cost=0.00..1.09 rows=1 width=85) (actual time=0.023..0.023 rows=0 loops=1)
+ Seq Scan on polygon_tbl (cost=0.00..1.09 rows=1 width=85) (actual time=0.023..0.023 rows=0.00 loops=1)
Filter: (f1 @> '((0.5,2))'::polygon)
Rows Removed by Filter: 7
Buffers: shared hit=1
@@ -858,7 +858,7 @@ EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';
QUERY PLAN
-------------------------------------------------------------------&zwsp;-------------------------------------------------------
- Index Scan using gpolygonind on polygon_tbl (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
+ Index Scan using gpolygonind on polygon_tbl (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0.00 loops=1)
Index Cond: (f1 @> '((0.5,2))'::polygon)
Rows Removed by Index Recheck: 1
Buffers: shared hit=1
@@ -888,13 +888,13 @@ EXPLAIN (ANALYZE, BUFFERS OFF) SELECT * FROM tenk1 WHERE unique1 < 100 AND un
QUERY PLAN
-------------------------------------------------------------------&zwsp;--------------------------------------------------------------
- Bitmap Heap Scan on tenk1 (cost=25.07..60.11 rows=10 width=244) (actual time=0.105..0.114 rows=10 loops=1)
+ Bitmap Heap Scan on tenk1 (cost=25.07..60.11 rows=10 width=244) (actual time=0.105..0.114 rows=10.00 loops=1)
Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
Heap Blocks: exact=10
- -> BitmapAnd (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
- -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
+ -> BitmapAnd (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0.00 loops=1)
+ -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100.00 loops=1)
Index Cond: (unique1 < 100)
- -> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
+ -> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999.00 loops=1)
Index Cond: (unique2 > 9000)
Planning Time: 0.162 ms
Execution Time: 0.143 ms
@@ -916,12 +916,12 @@ EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;
QUERY PLAN
-------------------------------------------------------------------&zwsp;-------------------------------------------------------------
- Update on tenk1 (cost=5.06..225.23 rows=0 width=0) (actual time=1.634..1.635 rows=0 loops=1)
- -> Bitmap Heap Scan on tenk1 (cost=5.06..225.23 rows=100 width=10) (actual time=0.065..0.141 rows=100 loops=1)
+ Update on tenk1 (cost=5.06..225.23 rows=0 width=0) (actual time=1.634..1.635 rows=0.00 loops=1)
+ -> Bitmap Heap Scan on tenk1 (cost=5.06..225.23 rows=100 width=10) (actual time=0.065..0.141 rows=100.00 loops=1)
Recheck Cond: (unique1 < 100)
Heap Blocks: exact=90
Buffers: shared hit=4 read=2
- -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
+ -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100.00 loops=1)
Index Cond: (unique1 < 100)
Buffers: shared read=2
Planning Time: 0.151 ms
@@ -1055,9 +1055,9 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000
QUERY PLAN
-------------------------------------------------------------------&zwsp;------------------------------------------------------------
- Limit (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
+ Limit (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2.00 loops=1)
Buffers: shared hit=16
- -> Index Scan using tenk1_unique2 on tenk1 (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
+ -> Index Scan using tenk1_unique2 on tenk1 (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2.00 loops=1)
Index Cond: (unique2 > 9000)
Filter: (unique1 < 100)
Rows Removed by Filter: 287
diff --git a/doc/src/sgml/planstats.sgml b/doc/src/sgml/planstats.sgml
index a909a5e313..068b804a18 100644
--- a/doc/src/sgml/planstats.sgml
+++ b/doc/src/sgml/planstats.sgml
@@ -492,7 +492,7 @@ SELECT relpages, reltuples FROM pg_class WHERE relname = 't';
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1;
QUERY PLAN
-------------------------------------------------------------------&zwsp;------------
- Seq Scan on t (cost=0.00..170.00 rows=100 width=8) (actual rows=100 loops=1)
+ Seq Scan on t (cost=0.00..170.00 rows=100 width=8) (actual rows=100.00 loops=1)
Filter: (a = 1)
Rows Removed by Filter: 9900
</programlisting>
@@ -509,7 +509,7 @@ EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1;
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
QUERY PLAN
-------------------------------------------------------------------&zwsp;----------
- Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=100 loops=1)
+ Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=100.00 loops=1)
Filter: ((a = 1) AND (b = 1))
Rows Removed by Filter: 9900
</programlisting>
@@ -533,7 +533,7 @@ ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
QUERY PLAN
-------------------------------------------------------------------&zwsp;------------
- Seq Scan on t (cost=0.00..195.00 rows=100 width=8) (actual rows=100 loops=1)
+ Seq Scan on t (cost=0.00..195.00 rows=100 width=8) (actual rows=100.00 loops=1)
Filter: ((a = 1) AND (b = 1))
Rows Removed by Filter: 9900
</programlisting>
@@ -554,9 +554,9 @@ EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT COUNT(*) FROM t GROUP BY a;
QUERY PLAN
-------------------------------------------------------------------&zwsp;----------------------
- HashAggregate (cost=195.00..196.00 rows=100 width=12) (actual rows=100 loops=1)
+ HashAggregate (cost=195.00..196.00 rows=100 width=12) (actual rows=100.00 loops=1)
Group Key: a
- -> Seq Scan on t (cost=0.00..145.00 rows=10000 width=4) (actual rows=10000 loops=1)
+ -> Seq Scan on t (cost=0.00..145.00 rows=10000 width=4) (actual rows=10000.00 loops=1)
</programlisting>
But without multivariate statistics, the estimate for the number of
groups in a query with two columns in <command>GROUP BY</command>, as
@@ -565,9 +565,9 @@ EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT COUNT(*) FROM t GROUP BY a;
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT COUNT(*) FROM t GROUP BY a, b;
QUERY PLAN
-------------------------------------------------------------------&zwsp;-------------------------
- HashAggregate (cost=220.00..230.00 rows=1000 width=16) (actual rows=100 loops=1)
+ HashAggregate (cost=220.00..230.00 rows=1000 width=16) (actual rows=100.00 loops=1)
Group Key: a, b
- -> Seq Scan on t (cost=0.00..145.00 rows=10000 width=8) (actual rows=10000 loops=1)
+ -> Seq Scan on t (cost=0.00..145.00 rows=10000 width=8) (actual rows=10000.00 loops=1)
</programlisting>
By redefining the statistics object to include n-distinct counts for the
two columns, the estimate is much improved:
@@ -578,9 +578,9 @@ ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT COUNT(*) FROM t GROUP BY a, b;
QUERY PLAN
-------------------------------------------------------------------&zwsp;-------------------------
- HashAggregate (cost=220.00..221.00 rows=100 width=16) (actual rows=100 loops=1)
+ HashAggregate (cost=220.00..221.00 rows=100 width=16) (actual rows=100.00 loops=1)
Group Key: a, b
- -> Seq Scan on t (cost=0.00..145.00 rows=10000 width=8) (actual rows=10000 loops=1)
+ -> Seq Scan on t (cost=0.00..145.00 rows=10000 width=8) (actual rows=10000.00 loops=1)
</programlisting>
</para>
@@ -618,7 +618,7 @@ ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
QUERY PLAN
-------------------------------------------------------------------&zwsp;------------
- Seq Scan on t (cost=0.00..195.00 rows=100 width=8) (actual rows=100 loops=1)
+ Seq Scan on t (cost=0.00..195.00 rows=100 width=8) (actual rows=100.00 loops=1)
Filter: ((a = 1) AND (b = 1))
Rows Removed by Filter: 9900
</programlisting>
@@ -675,7 +675,7 @@ SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid),
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 10;
QUERY PLAN
-------------------------------------------------------------------&zwsp;--------
- Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=0 loops=1)
+ Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=0.00 loops=1)
Filter: ((a = 1) AND (b = 10))
Rows Removed by Filter: 10000
</programlisting>
@@ -688,7 +688,7 @@ EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1
EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a <= 49 AND b > 49;
QUERY PLAN
-------------------------------------------------------------------&zwsp;--------
- Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=0 loops=1)
+ Seq Scan on t (cost=0.00..195.00 rows=1 width=8) (actual rows=0.00 loops=1)
Filter: ((a <= 49) AND (b > 49))
Rows Removed by Filter: 10000
</programlisting>
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index 652ece7213..7daddf03ef 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -500,11 +500,11 @@ EXPLAIN ANALYZE EXECUTE query(100, 200);
QUERY PLAN
-------------------------------------------------------------------&zwsp;------------------------------------------------------
- HashAggregate (cost=10.77..10.87 rows=10 width=12) (actual time=0.043..0.044 rows=10 loops=1)
+ HashAggregate (cost=10.77..10.87 rows=10 width=12) (actual time=0.043..0.044 rows=10.00 loops=1)
Group Key: foo
Batches: 1 Memory Usage: 24kB
Buffers: shared hit=4
- -> Index Scan using test_pkey on test (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99 loops=1)
+ -> Index Scan using test_pkey on test (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99.00 loops=1)
Index Cond: ((id > 100) AND (id < 200))
Buffers: shared hit=4
Planning Time: 0.244 ms
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 9fdf8b1d91..1d9924a2a3 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1029,8 +1029,8 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
With <command>EXPLAIN ANALYZE</command>, we see:
<programlisting>
- Aggregate (cost=21763.99..21764.00 rows=1 width=0) (actual time=188.180..188.181 rows=1 loops=1)
- -> Foreign Scan on words (cost=0.00..21761.41 rows=1032 width=0) (actual time=188.177..188.177 rows=0 loops=1)
+ Aggregate (cost=21763.99..21764.00 rows=1 width=0) (actual time=188.180..188.181 rows=1.00 loops=1)
+ -> Foreign Scan on words (cost=0.00..21761.41 rows=1032 width=0) (actual time=188.177..188.177 rows=0.00 loops=1)
Filter: (word = 'caterpiler'::text)
Rows Removed by Filter: 479829
Foreign File: /usr/share/dict/words
@@ -1042,8 +1042,8 @@ SELECT count(*) FROM words WHERE word = 'caterpiler';
If the materialized view is used instead, the query is much faster:
<programlisting>
- Aggregate (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
- -> Index Only Scan using wrd_word on wrd (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
+ Aggregate (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1.00 loops=1)
+ -> Index Only Scan using wrd_word on wrd (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0.00 loops=1)
Index Cond: (word = 'caterpiler'::text)
Heap Fetches: 0
Planning time: 0.164 ms
@@ -1073,11 +1073,11 @@ SELECT word FROM words ORDER BY word <-> 'caterpiler' LIMIT 10;
</programlisting>
<programlisting>
- Limit (cost=11583.61..11583.64 rows=10 width=32) (actual time=1431.591..1431.594 rows=10 loops=1)
- -> Sort (cost=11583.61..11804.76 rows=88459 width=32) (actual time=1431.589..1431.591 rows=10 loops=1)
+ Limit (cost=11583.61..11583.64 rows=10 width=32) (actual time=1431.591..1431.594 rows=10.00 loops=1)
+ -> Sort (cost=11583.61..11804.76 rows=88459 width=32) (actual time=1431.589..1431.591 rows=10.00 loops=1)
Sort Key: ((word <-> 'caterpiler'::text))
Sort Method: top-N heapsort Memory: 25kB
- -> Foreign Scan on words (cost=0.00..9672.05 rows=88459 width=32) (actual time=0.057..1286.455 rows=479829 loops=1)
+ -> Foreign Scan on words (cost=0.00..9672.05 rows=88459 width=32) (actual time=0.057..1286.455 rows=479829.00 loops=1)
Foreign File: /usr/share/dict/words
Foreign File Size: 4953699
Planning time: 0.128 ms
@@ -1087,8 +1087,8 @@ SELECT word FROM words ORDER BY word <-> 'caterpiler' LIMIT 10;
Using the materialized view:
<programlisting>
- Limit (cost=0.29..1.06 rows=10 width=10) (actual time=187.222..188.257 rows=10 loops=1)
- -> Index Scan using wrd_trgm on wrd (cost=0.29..37020.87 rows=479829 width=10) (actual time=187.219..188.252 rows=10 loops=1)
+ Limit (cost=0.29..1.06 rows=10 width=10) (actual time=187.222..188.257 rows=10.00 loops=1)
+ -> Index Scan using wrd_trgm on wrd (cost=0.29..37020.87 rows=479829 width=10) (actual time=187.219..188.252 rows=10.00 loops=1)
Order By: (word <-> 'caterpiler'::text)
Planning time: 0.196 ms
Execution time: 198.640 ms
--
2.34.1
On Mon, Feb 24, 2025 at 12:12 PM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:
If no one is concerned about rows being a non-integer, then I support
this, as it's quite strange for the average rows to be an integer only
for me. If we go with this approach, we should also update all examples
in the documentation accordingly. I attached patch with changes in
documentation.
Thanks, that's very helpful. If we go forward with this, I'll commit
your patch and mine together.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Feb 21, 2025 at 5:09 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Feb 21, 2025 at 7:04 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
A very significant fraction of the buildfarm is now pink.
If you don't have a fix pretty nearly ready, please revert.When we're going to do a release, you want no commits for at least 24
hours before the release so that we can make sure the buildfarm is
clean. But when I commit something and the buildfarm fails, you want
it reverted within a handful of hours before I can even be sure of
having seen all the failures the commit caused, let alone had time to
think about what the best fix might be. It doesn't make sense to me
that we need 24 hours to be sure that the buildfarm is passing, but in
3 hours I'm supposed to see all the failures -- including the ones
that only happen on buildfarm animals that run once a day, I guess? --
and analyze them -- and decide on a fix -- naturally without any sort
of discussion because there's no time for that -- and code the fix --
and push it. I have a really hard time seeing how that's a reasonable
expectation.I understand that the buildfarm can't be red all the time or nobody
can distinguish the problems they caused from preexisting ones.
I think you're right, and the solution has to be something like:
1. If it's > 24 hours until a release, then test failures on buildfarm
must be allowed.
2. Buildfarm needs to track failures at a fine enough granularity that
I can tell whether my change caused the failure.
I've worked at a few database companies -- some had workable test
frameworks, others didn't. The biggest mistake some companies made was
to collapse an entire test suite into a single, binary result: "ALL
PASSED" vs. "AT LEAST ONE FAILED." (It's fine to collapse the test
suite *before you merge your commit,* because then you are testing
*only* your commit. After you merge it, however, CI / buildfarm ends
up testing *multiple* new commits at the same time, so the tests need
to report at finer granularity.)
If we can't do CI / buildfarm at the granularity of a single commit
(and, at some point, no organization can -- the test universe becomes
too large!), then we have to design for the possibility that one or
more of multiple commits will cause a test failure, while the
remaining commits are innocent. And so the test framework has to
identify the (likely) culprit for a particular new test failure, and
then give the author *enough time* to fix that failure. And while the
author is fixing his failure, other authors need to be able to merge
their commits, etc.
(Note that actual *compilation* failures are more severe than test
failures, because if PostgreSQL can't compile, then no one can get any
work done if they rebase to TIP. But I think you are discussing tests,
not compilation.)
One company I worked for tracked test failures by individual unit
test. So, "parallel" schedule would have 220+ tests, each tracking
SUCCESS, [EXISTING] FAILURE, and NEW FAILURE. On every NEW FAILURE,
the system notified the unit test owner (this is feasible inside a
corporation, but perhaps not for an open source community) and
concurrently started running the equivalent of "git bisect" to
identify the offending commit. Once it identified (what it thought
was) the offending commit, it notified that committer as well.
In the case of my commit today, the failures are the result of a
2-line regression diff with no functional impact that neither CI nor
any of the 11 reviewers noticed.
This is exactly the sort of case where tracking NEW FAILURE at the
unit test level is useful. So you made a change that caused a
regression diff, and the new output is better than the old (or at
least neutral). Flag this individual unit test, send it to Robert, and
development continues.
Given enough unit tests, along with reasonable pre-commit CI testing,
the probability of two commits causing regressions / diffs in the same
unit test, over a 1-week period (let's say), is very small.
So: +1 to your proposal.
James
On Mon, Feb 24, 2025 at 2:16 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Feb 24, 2025 at 12:12 PM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:If no one is concerned about rows being a non-integer, then I support
this, as it's quite strange for the average rows to be an integer only
for me. If we go with this approach, we should also update all examples
in the documentation accordingly. I attached patch with changes in
documentation.Thanks, that's very helpful. If we go forward with this, I'll commit
your patch and mine together.
Since Tom doesn't seem to want to further object to trying it this
way, I've gone ahead and done this for now. Granted, it's not
altogether clear from the buildfarm that we would have ever gotten any
more failures after 44cbba9a7f51a3888d5087fc94b23614ba2b81f2, but it
seems to me that there's no way to rule out the possibility, and
low-frequency buildfarm failures that might only occur once a year are
in some ways more annoying than high-frequency ones, especially to
people who put a lot of energy into tracking down those failures. It
just seems unprincipled to me to leave things in a state where we know
that such failures are possible, and wonky to have things in a state
where whether parallel query gets used or not could make the
difference between getting a report of X rows and getting a report of
X.00 rows. If a user notices that difference in their own environment
-- regression tests aside -- they're not likely to guess what has
happened.
Of course, if this commit draws objections or runs into problems with
the buildfarm or with users, we might have to reconsider. I'm not dead
set on having it this way rather than any other. But I think it's more
principled than making 0-vs-2 digits predicated on a random event; and
it's a lot simpler than any of the other options that have been
proposed.
--
Robert Haas
EDB: http://www.enterprisedb.com
On 27.02.2025 19:51, Robert Haas wrote:
On Mon, Feb 24, 2025 at 2:16 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Feb 24, 2025 at 12:12 PM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:If no one is concerned about rows being a non-integer, then I support
this, as it's quite strange for the average rows to be an integer only
for me. If we go with this approach, we should also update all examples
in the documentation accordingly. I attached patch with changes in
documentation.Thanks, that's very helpful. If we go forward with this, I'll commit
your patch and mine together.Since Tom doesn't seem to want to further object to trying it this
way, I've gone ahead and done this for now. Granted, it's not
altogether clear from the buildfarm that we would have ever gotten any
more failures after 44cbba9a7f51a3888d5087fc94b23614ba2b81f2, but it
seems to me that there's no way to rule out the possibility, and
low-frequency buildfarm failures that might only occur once a year are
in some ways more annoying than high-frequency ones, especially to
people who put a lot of energy into tracking down those failures. It
just seems unprincipled to me to leave things in a state where we know
that such failures are possible, and wonky to have things in a state
where whether parallel query gets used or not could make the
difference between getting a report of X rows and getting a report of
X.00 rows. If a user notices that difference in their own environment
-- regression tests aside -- they're not likely to guess what has
happened.Of course, if this commit draws objections or runs into problems with
the buildfarm or with users, we might have to reconsider. I'm not dead
set on having it this way rather than any other. But I think it's more
principled than making 0-vs-2 digits predicated on a random event; and
it's a lot simpler than any of the other options that have been
proposed.
Then I am moving the commitfest entry [0]https://commitfest.postgresql.org/patch/5501/ to the Committed status.
[0]: https://commitfest.postgresql.org/patch/5501/
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Hi! I got a query plan with a strange number of rows. Could you please
help me understand it?
create temp table ta (id int primary key, val int);
create temp table tb (id int primary key, aval int);
create temp table tc (id int primary key, aid int);
insert into ta select id, id from generate_series(1,1000) as id;
insert into tb select id, id from generate_series(500,1000) as id;
insert into tc select id, id from generate_series(400,1000) as id;
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
SELECT 1
FROM ta ta1
WHERE EXISTS (SELECT 1
FROM tb
JOIN tc
ON tb.id = ta1.id and
ta1.id < 1000
where exists (select 1 from ta ta2 where ta2.id =
ta1.id));
QUERY PLAN
--------------------------------------------------------------------------------------
Seq Scan on ta ta1 (actual rows=500.00 loops=1)
Filter: EXISTS(SubPlan 2)
Rows Removed by Filter: 500
SubPlan 2
-> Result (actual rows=0.50 loops=1000)
One-Time Filter: ((ta1.id < 1000) AND (InitPlan 1).col1)
InitPlan 1
-> Index Only Scan using ta_pkey on ta ta2 (actual
rows=1.00 loops=999)
Index Cond: (id = ta1.id)
Heap Fetches: 999
-> Nested Loop (actual rows=0.50 loops=999)
-> Seq Scan on tb (actual rows=0.50 loops=999)
Filter: (id = ta1.id)
Rows Removed by Filter: 375
-> Seq Scan on tc (actual rows=1.00 loops=500)
(15 rows)
To be honest I can't understand why 0.50 number of rows here?
--
Regards,
Alena Rybakina
Postgres Professional
On Thu, 6 Mar 2025 at 14:18, Alena Rybakina <a.rybakina@postgrespro.ru> wrote:
Hi! I got a query plan with a strange number of rows. Could you please
help me understand it?To be honest I can't understand why 0.50 number of rows here?
Because the scan matched only ~(500 rows over 999 iterations = 500/999
~=) 0.50 rows for every loop, on average, for these plan nodes:
-> Nested Loop (actual rows=0.50 loops=999)
-> Seq Scan on tb (actual rows=0.50 loops=999)
And for this, it was 500 rows total in 1000 iterations, which also
rounds to 0.50:
SubPlan 2
-> Result (actual rows=0.50 loops=1000)
One-Time Filter: ((ta1.id < 1000) AND (InitPlan 1).col1)
As of ddb17e38 (and its follow-up 95dbd827), we display fractional
rows-per-loop, with 2 digits of precision, rather than a rounded
integer. This allows a user to distinguish plan nodes with 0.49
rows/loop and 0.01 rows/loop, and that can help inform the user about
how to further optimize their usage of indexes and other optimization
paths.
Kind regards,
Matthias van de Meent
Neon (https://neon.tech)
On Thu, Mar 6, 2025 at 8:30 AM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:
On Thu, 6 Mar 2025 at 14:18, Alena Rybakina <a.rybakina@postgrespro.ru> wrote:
Hi! I got a query plan with a strange number of rows. Could you please
help me understand it?To be honest I can't understand why 0.50 number of rows here?
Because the scan matched only ~(500 rows over 999 iterations = 500/999
~=) 0.50 rows for every loop, on average, for these plan nodes:
This is a good and correct explanation, but I'm VERY curious to hear
more from Alena. Like, Tom expressed the concern before we did this
that the fractional digits would confuse people, and the fact that
someone who is a regular participant on this mailing list was one of
the people confused gives credence to that concern. But I want to know
what exactly Alena found (or finds) confusing here. The Nested Loop
executes 999 times, so perhaps Alena thought that 0.50 was the TOTAL
number of rows across ALL of those executions rather than the AVERAGE
number of rows per execution? Because then 0.50 would indeed be a very
surprising result. Or maybe she just didn't realize that part of the
plan executed 999 times? Or something else?
Alena, if you're willing, please elaborate on what you think is confusing here!
--
Robert Haas
EDB: http://www.enterprisedb.com
On 06.03.2025 17:13, Robert Haas wrote:
On Thu, Mar 6, 2025 at 8:30 AM Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:On Thu, 6 Mar 2025 at 14:18, Alena Rybakina <a.rybakina@postgrespro.ru> wrote:
Hi! I got a query plan with a strange number of rows. Could you please
help me understand it?To be honest I can't understand why 0.50 number of rows here?
Because the scan matched only ~(500 rows over 999 iterations = 500/999
~=) 0.50 rows for every loop, on average, for these plan nodes:This is a good and correct explanation, but I'm VERY curious to hear
more from Alena. Like, Tom expressed the concern before we did this
that the fractional digits would confuse people, and the fact that
someone who is a regular participant on this mailing list was one of
the people confused gives credence to that concern. But I want to know
what exactly Alena found (or finds) confusing here. The Nested Loop
executes 999 times, so perhaps Alena thought that 0.50 was the TOTAL
number of rows across ALL of those executions rather than the AVERAGE
number of rows per execution? Because then 0.50 would indeed be a very
surprising result. Or maybe she just didn't realize that part of the
plan executed 999 times? Or something else?Alena, if you're willing, please elaborate on what you think is confusing here!
To be honest, I initially took it as the total number of tuples and
couldn't figure out for myself how to interpret the result - 0 tuples or
1 tuple in the end. Maybe it wasn't quite correct to perceive it that
way, but Matthias's explanation helped me figure out the reason why such
a strange result was obtained, although it's not usual to see it.
--
Regards,
Alena Rybakina
Postgres Professional
On 06.03.2025 16:30, Matthias van de Meent wrote:
On Thu, 6 Mar 2025 at 14:18, Alena Rybakina <a.rybakina@postgrespro.ru> wrote:
Hi! I got a query plan with a strange number of rows. Could you please
help me understand it?To be honest I can't understand why 0.50 number of rows here?
Because the scan matched only ~(500 rows over 999 iterations = 500/999
~=) 0.50 rows for every loop, on average, for these plan nodes:-> Nested Loop (actual rows=0.50 loops=999)
-> Seq Scan on tb (actual rows=0.50 loops=999)And for this, it was 500 rows total in 1000 iterations, which also
rounds to 0.50:SubPlan 2
-> Result (actual rows=0.50 loops=1000)
One-Time Filter: ((ta1.id < 1000) AND (InitPlan 1).col1)As of ddb17e38 (and its follow-up 95dbd827), we display fractional
rows-per-loop, with 2 digits of precision, rather than a rounded
integer. This allows a user to distinguish plan nodes with 0.49
rows/loop and 0.01 rows/loop, and that can help inform the user about
how to further optimize their usage of indexes and other optimization
paths.
Thanks for the explanation. Now I understand.
--
Regards,
Alena Rybakina
Postgres Professional
On Thu, Mar 6, 2025 at 4:18 PM Alena Rybakina <a.rybakina@postgrespro.ru> wrote:
To be honest, I initially took it as the total number of tuples and
couldn't figure out for myself how to interpret the result - 0 tuples or
1 tuple in the end. Maybe it wasn't quite correct to perceive it that
way, but Matthias's explanation helped me figure out the reason why such
a strange result was obtained, although it's not usual to see it.
Yeah, I thought the same back when I first started using PostgreSQL
and had to learn that it wasn't the case. I still think that's what we
*should* be displaying -- I think dividing by nloops obscures the data
users actually want. But it would admittedly be a pretty big
compatibility break at this point, and at least if we display a couple
of decimal places it's more possible to approximate the undivided
value.
--
Robert Haas
EDB: http://www.enterprisedb.com
Hi,
In the stats_ext regression test, there is a function
check_estimated_rows that returns actual rows as an integer. Currently,
none of the test cases produce a non-zero fractional part in actual rows.
The question is: should this function be modified to return a fractional
number instead of an integer?
Personally, I don’t see much value in doing so, because the purpose of
the test is to compare the estimated row count with the actual number of
rows. It is also unlikely that there will be test cases where loops > 1,
and the presence of a fractional part does not change the essence of the
test.
What do you think?
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
On Tue, Mar 11, 2025 at 5:58 AM Ilia Evdokimov
<ilya.evdokimov@tantorlabs.com> wrote:
In the stats_ext regression test, there is a function
check_estimated_rows that returns actual rows as an integer. Currently,
none of the test cases produce a non-zero fractional part in actual rows.The question is: should this function be modified to return a fractional
number instead of an integer?Personally, I don’t see much value in doing so, because the purpose of
the test is to compare the estimated row count with the actual number of
rows. It is also unlikely that there will be test cases where loops > 1,
and the presence of a fractional part does not change the essence of the
test.What do you think?
I suppose the way this is currently coded, it will have the effect of
extracting the integer part of the row estimate. That doesn't really
seem like a bad idea to me, so I'm not sure it's worth trying to
adjust anything here.
One thing that I just noticed, though, is that we added two decimal
places to the actual row count, but not the estimated row count. So
now we get stuff like this:
-> Seq Scan on pgbench_branches b (cost=0.00..1.10 rows=10
width=364) (actual time=0.007..0.009 rows=10.00 loops=1)
But why isn't it just as valuable to have two decimal places for the
estimate? I theorize that the cases that are really a problem here are
those where the row count estimate is between 0 and 1 per row, and
rounding to an integer loses all precision.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
But why isn't it just as valuable to have two decimal places for the
estimate? I theorize that the cases that are really a problem here are
those where the row count estimate is between 0 and 1 per row, and
rounding to an integer loses all precision.
Currently, the planner rounds *all* rowcount estimates to integers
(cf. clamp_row_est()). Maybe it'd be appropriate to rethink that,
but it's not just a matter of changing EXPLAIN's print format.
regards, tom lane
On Mon, Mar 31, 2025 at 1:49 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
But why isn't it just as valuable to have two decimal places for the
estimate? I theorize that the cases that are really a problem here are
those where the row count estimate is between 0 and 1 per row, and
rounding to an integer loses all precision.Currently, the planner rounds *all* rowcount estimates to integers
(cf. clamp_row_est()). Maybe it'd be appropriate to rethink that,
but it's not just a matter of changing EXPLAIN's print format.
Oh, right. I've never really understood why we round off to integers,
but the fact that we don't allow row counts < 1 feels like something
pretty important. My intuition is that it probably helps a lot more
than it hurts, too.
--
Robert Haas
EDB: http://www.enterprisedb.com
On 31.03.2025 22:09, Robert Haas wrote:
Oh, right. I've never really understood why we round off to integers,
but the fact that we don't allow row counts < 1 feels like something
pretty important. My intuition is that it probably helps a lot more
than it hurts, too.
We definitely shouldn’t remove the row counts < 1 check, since there are
many places in the planner where we divide by rows. This mechanism was
added specifically to prevent division by zero. Also, allowing rows
estimates below 1 can sometimes make the planner overly optimistic,
leading it to prefer cheaper-looking plans that may not perform well in
practice. For example, choosing a Nested Loop instead of a more
appropriate Hash Join.
Allowing fractional rows > 1 might help improve planner accuracy in some
cases, but this needs further study to fully understand the impact.
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
On 3/31/25 19:35, Robert Haas wrote:
But why isn't it just as valuable to have two decimal places for the
estimate? I theorize that the cases that are really a problem here are
those where the row count estimate is between 0 and 1 per row, and
rounding to an integer loses all precision.
Issues I've ever seen were about zero rows number - we lose
understanding of what the job the executor has done in the node - loops
= 10000000 doesn't tell us any helpful information in that case.
Maybe in the next version, we replace the two fractional numbers rule
with two valuable numbers. At least, it removes boring xxx.00 numbers
from EXPLAIN.
--
regards, Andrei Lepikhov
On 31.03.2025 23:59, Ilia Evdokimov wrote:
We definitely shouldn’t remove the row counts < 1 check, since there
are many places in the planner where we divide by rows. This mechanism
was added specifically to prevent division by zero. Also, allowing
rows estimates below 1 can sometimes make the planner overly
optimistic, leading it to prefer cheaper-looking plans that may not
perform well in practice. For example, choosing a Nested Loop instead
of a more appropriate Hash Join.Allowing fractional rows > 1 might help improve planner accuracy in
some cases, but this needs further study to fully understand the impact.
I've been investigating whether it's worth removing rounding in row
estimates - and I believe it is.
[ v1-0001-Always-use-two-fractional-digits-for-estimated-rows_SRC.patch ]
Currently, we round most row estimates using rint() inside
clamp_row_est(). However, this function is also used for rounding tuples
and page counts. These should remain integral, but row estimates can and
should remain fractional for better precision. To address this, I
introduced a new function clamp_tuple_est() which retains the existing
rounding behavior (via rint()), while clamp_tuple_est() no longer
rounds. I use clamp_tuple_est() only for row estimates and
clamp_tuple_est() for tuples and pages elsewhere.
After removing rounding, two small issues needed fixing. First, there
was a check rows > 1 in the cost estimation for Nested Loop joins, which
is no longer reliable for values like 1.3. I updated it to rows >= 2 to
retain the original behavior. This can be refined further, but, in my
opinion, it's a practical compromise. Second, there is still a call to
rint() in cost of mergejoin which likely should be removed too - though
I haven’t included that here yet.
Also, if we're no longer rounding estimates, EXPLAIN should display them
with two decimal digits, just like it already does for actual rows.
[ v1-0002-Always-use-two-fractional-digits-for-estimated-rows_TESTS.patch ]
So, what does this change improve? Here are some of the observed plan
improvements:
* Previously, a Parallel Aggregate was chosen. With slightly more
accurate estimation, the planner switches to FinalizeAggregate,
which can be more efficient in distributed plans.
* In certain nested joins with constant subqueries, the planner
previously inserted an unnecessary Materialize. With improved
estimates, it now skips that step, reducing memory usage and latency.
* When the estimated number of iterations becomes non-integer but
still justifies caching, the planner adds Memoize instead of
re-running a function like generate_series(). This can speed up
execution significantly.
* In one case involving partitioned tables and filter conditions like
t1.b = 0, the planner now chooses an index-based nested loop join
instead of a hash join. This results in a more efficient plan with
fewer memory and CPU costs.
I know this patch still needs documentation updates to describe the new
estimation display and behaviors. But before that, I’d like to gather
feedback: does community agree that more precise estimates and
fractional values are better than always rounding?
If anyone would like to see the EXPLAIN ANALYZE VERBOSE output of
changes in regression tests, I’d be happy to share them.
All feedback and suggestions welcome!
--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC.
Attachments:
v1-0001-Always-use-two-fractional-digits-for-estimated-rows_SRC.patchtext/x-patch; charset=UTF-8; name=v1-0001-Always-use-two-fractional-digits-for-estimated-rows_SRC.patchDownload
From a042ee22dbdae5049f4830116e83a3a72ea5e405 Mon Sep 17 00:00:00 2001
From: Evdokimov Ilia <ilya.evdokimov@tantorlabs.com>
Date: Wed, 9 Apr 2025 22:24:01 +0300
Subject: [PATCH v1] Always use two fractional digits for estimated rows count
---
contrib/file_fdw/file_fdw.c | 4 +-
contrib/postgres_fdw/postgres_fdw.c | 4 +-
contrib/tsm_system_rows/tsm_system_rows.c | 4 +-
contrib/tsm_system_time/tsm_system_time.c | 4 +-
src/backend/access/table/tableam.c | 2 +-
src/backend/access/tablesample/bernoulli.c | 2 +-
src/backend/access/tablesample/system.c | 4 +-
src/backend/commands/explain.c | 4 +-
src/backend/optimizer/path/costsize.c | 53 +++++++++++++++-------
src/backend/optimizer/util/pathnode.c | 4 +-
src/backend/utils/adt/selfuncs.c | 14 +++---
src/include/optimizer/optimizer.h | 1 +
12 files changed, 61 insertions(+), 39 deletions(-)
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index a9a5671d95a..682bb86adde 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -1097,7 +1097,7 @@ estimate_size(PlannerInfo *root, RelOptInfo *baserel,
double density;
density = baserel->tuples / (double) baserel->pages;
- ntuples = clamp_row_est(density * (double) pages);
+ ntuples = clamp_tuple_est(density * (double) pages);
}
else
{
@@ -1113,7 +1113,7 @@ estimate_size(PlannerInfo *root, RelOptInfo *baserel,
tuple_width = MAXALIGN(baserel->reltarget->width) +
MAXALIGN(SizeofHeapTupleHeader);
- ntuples = clamp_row_est((double) stat_buf.st_size /
+ ntuples = clamp_tuple_est((double) stat_buf.st_size /
(double) tuple_width);
}
fdw_private->ntuples = ntuples;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a7e0cc9f323..5e1c7bb32e1 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -3278,7 +3278,7 @@ estimate_path_cost_size(PlannerInfo *root,
* Back into an estimate of the number of retrieved rows. Just in
* case this is nuts, clamp to at most nrows.
*/
- retrieved_rows = clamp_row_est(rows / fpinfo->local_conds_sel);
+ retrieved_rows = clamp_tuple_est(rows / fpinfo->local_conds_sel);
retrieved_rows = Min(retrieved_rows, nrows);
/*
@@ -3453,7 +3453,7 @@ estimate_path_cost_size(PlannerInfo *root,
* Back into an estimate of the number of retrieved rows. Just in
* case this is nuts, clamp to at most foreignrel->tuples.
*/
- retrieved_rows = clamp_row_est(rows / fpinfo->local_conds_sel);
+ retrieved_rows = clamp_tuple_est(rows / fpinfo->local_conds_sel);
retrieved_rows = Min(retrieved_rows, foreignrel->tuples);
/*
diff --git a/contrib/tsm_system_rows/tsm_system_rows.c b/contrib/tsm_system_rows/tsm_system_rows.c
index f401efa2131..308bb301da1 100644
--- a/contrib/tsm_system_rows/tsm_system_rows.c
+++ b/contrib/tsm_system_rows/tsm_system_rows.c
@@ -135,7 +135,7 @@ system_rows_samplescangetsamplesize(PlannerInfo *root,
/* Clamp to the estimated relation size */
if (ntuples > baserel->tuples)
ntuples = (int64) baserel->tuples;
- ntuples = clamp_row_est(ntuples);
+ ntuples = clamp_tuple_est(ntuples);
if (baserel->tuples > 0 && baserel->pages > 0)
{
@@ -151,7 +151,7 @@ system_rows_samplescangetsamplesize(PlannerInfo *root,
}
/* Clamp to sane value */
- npages = clamp_row_est(Min((double) baserel->pages, npages));
+ npages = clamp_tuple_est(Min((double) baserel->pages, npages));
*pages = npages;
*tuples = ntuples;
diff --git a/contrib/tsm_system_time/tsm_system_time.c b/contrib/tsm_system_time/tsm_system_time.c
index c9c71d8c3af..b2d1d7e37d7 100644
--- a/contrib/tsm_system_time/tsm_system_time.c
+++ b/contrib/tsm_system_time/tsm_system_time.c
@@ -151,7 +151,7 @@ system_time_samplescangetsamplesize(PlannerInfo *root,
npages = millis; /* even more bogus, but whatcha gonna do? */
/* Clamp to sane value */
- npages = clamp_row_est(Min((double) baserel->pages, npages));
+ npages = clamp_tuple_est(Min((double) baserel->pages, npages));
if (baserel->tuples > 0 && baserel->pages > 0)
{
@@ -167,7 +167,7 @@ system_time_samplescangetsamplesize(PlannerInfo *root,
}
/* Clamp to the estimated relation size */
- ntuples = clamp_row_est(Min(baserel->tuples, ntuples));
+ ntuples = clamp_tuple_est(Min(baserel->tuples, ntuples));
*pages = npages;
*tuples = ntuples;
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index a56c5eceb14..e14a315f319 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -742,7 +742,7 @@ table_block_relation_estimate_size(Relation rel, int32 *attr_widths,
/* note: integer division is intentional here */
density = (usable_bytes_per_page * fillfactor / 100) / tuple_width;
/* There's at least one row on the page, even with low fillfactor. */
- density = clamp_row_est(density);
+ density = clamp_tuple_est(density);
}
*tuples = rint(density * (double) curpages);
diff --git a/src/backend/access/tablesample/bernoulli.c b/src/backend/access/tablesample/bernoulli.c
index 5e1c5d2b723..d1993299158 100644
--- a/src/backend/access/tablesample/bernoulli.c
+++ b/src/backend/access/tablesample/bernoulli.c
@@ -117,7 +117,7 @@ bernoulli_samplescangetsamplesize(PlannerInfo *root,
/* We'll visit all pages of the baserel */
*pages = baserel->pages;
- *tuples = clamp_row_est(baserel->tuples * samplefract);
+ *tuples = clamp_tuple_est(baserel->tuples * samplefract);
}
/*
diff --git a/src/backend/access/tablesample/system.c b/src/backend/access/tablesample/system.c
index 8db813b89fc..8944fe28799 100644
--- a/src/backend/access/tablesample/system.c
+++ b/src/backend/access/tablesample/system.c
@@ -117,10 +117,10 @@ system_samplescangetsamplesize(PlannerInfo *root,
}
/* We'll visit a sample of the pages ... */
- *pages = clamp_row_est(baserel->pages * samplefract);
+ *pages = clamp_tuple_est(baserel->pages * samplefract);
/* ... and hopefully get a representative number of tuples from them */
- *tuples = clamp_row_est(baserel->tuples * samplefract);
+ *tuples = clamp_tuple_est(baserel->tuples * samplefract);
}
/*
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ef8aa489af8..9d9da4880a3 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1808,7 +1808,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
{
if (es->format == EXPLAIN_FORMAT_TEXT)
{
- appendStringInfo(es->str, " (cost=%.2f..%.2f rows=%.0f width=%d)",
+ appendStringInfo(es->str, " (cost=%.2f..%.2f rows=%.2f width=%d)",
plan->startup_cost, plan->total_cost,
plan->plan_rows, plan->plan_width);
}
@@ -1819,7 +1819,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
ExplainPropertyFloat("Total Cost", NULL, plan->total_cost,
2, es);
ExplainPropertyFloat("Plan Rows", NULL, plan->plan_rows,
- 0, es);
+ 2, es);
ExplainPropertyInteger("Plan Width", NULL, plan->plan_width,
es);
}
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 60b0fcfb6be..27f8d70eb2c 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -205,6 +205,29 @@ static double page_size(double tuples, int width);
static double get_parallel_divisor(Path *path);
+/*
+ * clamp_tuple_est
+ * Force a tuple-count estimate to a sane value.
+ */
+double
+clamp_tuple_est(double ntuples)
+{
+ /*
+ * Avoid infinite and NaN tuple estimates. Costs derived from such values
+ * are going to be useless. Also force the estimate to be at least one
+ * tuple, to make explain output look better and to avoid possible
+ * divide-by-zero when interpolating costs. Make it an integer, too.
+ */
+ if (ntuples > MAXIMUM_ROWCOUNT || isnan(ntuples))
+ ntuples = MAXIMUM_ROWCOUNT;
+ else if (ntuples <= 1.0)
+ ntuples = 1.0;
+ else
+ ntuples = rint(ntuples);
+
+ return ntuples;
+}
+
/*
* clamp_row_est
* Force a row-count estimate to a sane value.
@@ -216,14 +239,12 @@ clamp_row_est(double nrows)
* Avoid infinite and NaN row estimates. Costs derived from such values
* are going to be useless. Also force the estimate to be at least one
* row, to make explain output look better and to avoid possible
- * divide-by-zero when interpolating costs. Make it an integer, too.
+ * divide-by-zero when interpolating costs.
*/
if (nrows > MAXIMUM_ROWCOUNT || isnan(nrows))
nrows = MAXIMUM_ROWCOUNT;
else if (nrows <= 1.0)
nrows = 1.0;
- else
- nrows = rint(nrows);
return nrows;
}
@@ -249,7 +270,7 @@ clamp_width_est(int64 tuple_width)
return (int32) MaxAllocSize;
/*
- * Unlike clamp_row_est, we just Assert that the value isn't negative,
+ * Unlike clamp_tuple_est, we just Assert that the value isn't negative,
* rather than masking such errors.
*/
Assert(tuple_width >= 0);
@@ -643,7 +664,7 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
run_cost += indexTotalCost - indexStartupCost;
/* estimate number of main-table tuples fetched */
- tuples_fetched = clamp_row_est(indexSelectivity * baserel->tuples);
+ tuples_fetched = clamp_tuple_est(indexSelectivity * baserel->tuples);
/* fetch estimated page costs for tablespace containing table */
get_tablespace_page_costs(baserel->reltablespace,
@@ -901,7 +922,7 @@ extract_nonindex_conditions(List *qual_clauses, List *indexclauses)
* computed for us by make_one_rel.
*
* Caller is expected to have ensured that tuples_fetched is greater than zero
- * and rounded to integer (see clamp_row_est). The result will likewise be
+ * and rounded to integer (see clamp_tuple_est). The result will likewise be
* greater than zero and integral.
*/
double
@@ -3084,7 +3105,7 @@ get_windowclause_startup_tuples(PlannerInfo *root, WindowClause *wc,
* subnode.
*/
- return clamp_row_est(return_tuples);
+ return clamp_tuple_est(return_tuples);
}
/*
@@ -3298,7 +3319,7 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
*/
startup_cost += outer_path->startup_cost + inner_path->startup_cost;
run_cost += outer_path->total_cost - outer_path->startup_cost;
- if (outer_path_rows > 1)
+ if (outer_path_rows >= 2)
run_cost += (outer_path_rows - 1) * inner_rescan_start_cost;
inner_run_cost = inner_path->total_cost - inner_path->startup_cost;
@@ -3323,7 +3344,7 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
{
/* Normal case; we'll scan whole input rel for each outer row */
run_cost += inner_run_cost;
- if (outer_path_rows > 1)
+ if (outer_path_rows >= 2)
run_cost += (outer_path_rows - 1) * inner_rescan_run_cost;
}
@@ -3656,8 +3677,8 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
* Convert selectivities to row counts. We force outer_rows and
* inner_rows to be at least 1, but the skip_rows estimates can be zero.
*/
- outer_skip_rows = rint(outer_path_rows * outerstartsel);
- inner_skip_rows = rint(inner_path_rows * innerstartsel);
+ outer_skip_rows = outer_path_rows * outerstartsel;
+ inner_skip_rows = inner_path_rows * innerstartsel;
outer_rows = clamp_row_est(outer_path_rows * outerendsel);
inner_rows = clamp_row_est(inner_path_rows * innerendsel);
@@ -4415,7 +4436,7 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
* that way, so it will be unable to drive the batch size below hash_mem
* when this is true.)
*/
- if (relation_byte_size(clamp_row_est(inner_path_rows * innermcvfreq),
+ if (relation_byte_size(clamp_tuple_est(inner_path_rows * innermcvfreq),
inner_path->pathtarget->width) > get_hash_memory_limit())
startup_cost += disable_cost;
@@ -4449,7 +4470,7 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
* to clamp inner_scan_frac to at most 1.0; but since match_count is
* at least 1, no such clamp is needed now.)
*/
- outer_matched_rows = rint(outer_path_rows * extra->semifactors.outer_match_frac);
+ outer_matched_rows = outer_path_rows * extra->semifactors.outer_match_frac;
inner_scan_frac = 2.0 / (extra->semifactors.match_count + 1.0);
startup_cost += hash_qual_cost.startup;
@@ -4573,7 +4594,7 @@ cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan)
if (subplan->subLinkType == EXISTS_SUBLINK)
{
/* we only need to fetch 1 tuple; clamp to avoid zero divide */
- sp_cost.per_tuple += plan_run_cost / clamp_row_est(plan->plan_rows);
+ sp_cost.per_tuple += plan_run_cost / clamp_tuple_est(plan->plan_rows);
}
else if (subplan->subLinkType == ALL_SUBLINK ||
subplan->subLinkType == ANY_SUBLINK)
@@ -6517,7 +6538,7 @@ compute_bitmap_pages(PlannerInfo *root, RelOptInfo *baserel,
/*
* Estimate number of main-table pages fetched.
*/
- tuples_fetched = clamp_row_est(indexSelectivity * baserel->tuples);
+ tuples_fetched = clamp_tuple_est(indexSelectivity * baserel->tuples);
T = (baserel->pages > 1) ? (double) baserel->pages : 1.0;
@@ -6583,7 +6604,7 @@ compute_bitmap_pages(PlannerInfo *root, RelOptInfo *baserel,
*/
if (lossy_pages > 0)
tuples_fetched =
- clamp_row_est(indexSelectivity *
+ clamp_tuple_est(indexSelectivity *
(exact_pages / heap_pages) * baserel->tuples +
(lossy_pages / heap_pages) * baserel->tuples);
}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 93e73cb44db..0b9ec6b26fc 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1687,7 +1687,7 @@ create_memoize_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
pathnode->param_exprs = param_exprs;
pathnode->singlerow = singlerow;
pathnode->binary_mode = binary_mode;
- pathnode->calls = clamp_row_est(calls);
+ pathnode->calls = clamp_tuple_est(calls);
/*
* For now we set est_entries to 0. cost_memoize_rescan() does all the
@@ -4049,7 +4049,7 @@ adjust_limit_rows_costs(double *rows, /* in/out parameter */
if (offset_est > 0)
offset_rows = (double) offset_est;
else
- offset_rows = clamp_row_est(input_rows * 0.10);
+ offset_rows = clamp_tuple_est(input_rows * 0.10);
if (offset_rows > *rows)
offset_rows = *rows;
if (input_rows > 0)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 588d991fa57..0c077c09175 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -2183,7 +2183,7 @@ estimate_array_length(PlannerInfo *root, Node *arrayexpr)
ATTSTATSSLOT_NUMBERS))
{
if (sslot.nnumbers > 0)
- nelem = clamp_row_est(sslot.numbers[sslot.nnumbers - 1]);
+ nelem = clamp_tuple_est(sslot.numbers[sslot.nnumbers - 1]);
free_attstatsslot(&sslot);
}
}
@@ -3462,7 +3462,7 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
* estimate is usually already at least 1, but clamp it just in case it
* isn't.
*/
- input_rows = clamp_row_est(input_rows);
+ input_rows = clamp_tuple_est(input_rows);
/*
* If no grouping columns, there's exactly one group. (This can't happen
@@ -3755,7 +3755,7 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
(1 - pow((rel->tuples - rel->rows) / rel->tuples,
rel->tuples / reldistinct));
}
- reldistinct = clamp_row_est(reldistinct);
+ reldistinct = clamp_tuple_est(reldistinct);
/*
* Update estimate of total distinct groups.
@@ -4071,7 +4071,7 @@ estimate_hash_bucket_stats(PlannerInfo *root, Node *hashkey, double nbuckets,
if (vardata.rel && vardata.rel->tuples > 0)
{
ndistinct *= vardata.rel->rows / vardata.rel->tuples;
- ndistinct = clamp_row_est(ndistinct);
+ ndistinct = clamp_tuple_est(ndistinct);
}
/*
@@ -6168,7 +6168,7 @@ get_variable_numdistinct(VariableStatData *vardata, bool *isdefault)
* If we had an absolute estimate, use that.
*/
if (stadistinct > 0.0)
- return clamp_row_est(stadistinct);
+ return clamp_tuple_est(stadistinct);
/*
* Otherwise we need to get the relation size; punt if not available.
@@ -6189,7 +6189,7 @@ get_variable_numdistinct(VariableStatData *vardata, bool *isdefault)
* If we had a relative estimate, use that.
*/
if (stadistinct < 0.0)
- return clamp_row_est(-stadistinct * ntuples);
+ return clamp_tuple_est(-stadistinct * ntuples);
/*
* With no data, estimate ndistinct = ntuples if the table is small, else
@@ -6197,7 +6197,7 @@ get_variable_numdistinct(VariableStatData *vardata, bool *isdefault)
* that the behavior isn't discontinuous.
*/
if (ntuples < DEFAULT_NUM_DISTINCT)
- return clamp_row_est(ntuples);
+ return clamp_tuple_est(ntuples);
*isdefault = true;
return DEFAULT_NUM_DISTINCT;
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 546828b54bd..e1be9dc6175 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -90,6 +90,7 @@ extern PGDLLIMPORT double recursive_worktable_factor;
extern PGDLLIMPORT int effective_cache_size;
extern double clamp_row_est(double nrows);
+extern double clamp_tuple_est(double ntuples);
extern int32 clamp_width_est(int64 tuple_width);
extern long clamp_cardinality_to_long(Cardinality x);
--
2.34.1
v1-0002-Always-use-two-fractional-digits-for-estimated-rows_TESTS.patchtext/x-patch; charset=UTF-8; name=v1-0002-Always-use-two-fractional-digits-for-estimated-rows_TESTS.patchDownload
From a34fd63c47a87e79d6b0fe0a705c527a5b2b22bc Mon Sep 17 00:00:00 2001
From: Evdokimov Ilia <ilya.evdokimov@tantorlabs.com>
Date: Wed, 9 Apr 2025 22:26:16 +0300
Subject: [PATCH v1] Always use two fractional digits for estimated rows count
---
.../expected/pg_overexplain.out | 12 +-
.../postgres_fdw/expected/postgres_fdw.out | 15 +-
src/test/regress/expected/explain.out | 146 +++++++++---------
src/test/regress/expected/inherit.out | 8 +-
src/test/regress/expected/join.out | 134 ++++++++--------
src/test/regress/expected/join_hash.out | 19 +--
src/test/regress/expected/misc_functions.out | 84 +++++-----
src/test/regress/expected/partition_join.out | 112 +++++++-------
src/test/regress/expected/select_views.out | 19 ++-
src/test/regress/expected/stats_ext.out | 12 +-
src/test/regress/sql/stats_ext.sql | 4 +-
11 files changed, 277 insertions(+), 288 deletions(-)
diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 28252dbff6c..731a0bf79a2 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -25,9 +25,9 @@ ERROR: unrecognized EXPLAIN option "debuff"
LINE 1: EXPLAIN (DEBUFF) SELECT 1;
^
EXPLAIN (DEBUG) SELECT 1;
- QUERY PLAN
-------------------------------------------
- Result (cost=0.00..0.01 rows=1 width=4)
+ QUERY PLAN
+---------------------------------------------
+ Result (cost=0.00..0.01 rows=1.00 width=4)
Disabled Nodes: 0
Parallel Safe: false
Plan Node ID: 0
@@ -41,9 +41,9 @@ EXPLAIN (DEBUG) SELECT 1;
(11 rows)
EXPLAIN (RANGE_TABLE) SELECT 1;
- QUERY PLAN
-------------------------------------------
- Result (cost=0.00..0.01 rows=1 width=4)
+ QUERY PLAN
+---------------------------------------------
+ Result (cost=0.00..0.01 rows=1.00 width=4)
RTI 1 (result):
Eref: "*RESULT*" ()
(3 rows)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index d1acee5a5fa..e5b92d2017b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -10151,13 +10151,16 @@ SELECT t1.a,t2.b,t3.c FROM fprt1 t1 INNER JOIN fprt2 t2 ON (t1.a = t2.b) INNER J
-- left outer join + nullable clause
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.a,t2.b,t2.c FROM fprt1 t1 LEFT JOIN (SELECT * FROM fprt2 WHERE a < 10) t2 ON (t1.a = t2.b and t1.b = t2.a) WHERE t1.a < 10 ORDER BY 1,2,3;
- QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Foreign Scan
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
Output: t1.a, fprt2.b, fprt2.c
- Relations: (public.ftprt1_p1 t1) LEFT JOIN (public.ftprt2_p1 fprt2)
- Remote SQL: SELECT r5.a, r6.b, r6.c FROM (public.fprt1_p1 r5 LEFT JOIN public.fprt2_p1 r6 ON (((r5.a = r6.b)) AND ((r5.b = r6.a)) AND ((r6.a < 10)))) WHERE ((r5.a < 10)) ORDER BY r5.a ASC NULLS LAST, r6.b ASC NULLS LAST, r6.c ASC NULLS LAST
-(4 rows)
+ Sort Key: t1.a, fprt2.b, fprt2.c
+ -> Foreign Scan
+ Output: t1.a, fprt2.b, fprt2.c
+ Relations: (public.ftprt1_p1 t1) LEFT JOIN (public.ftprt2_p1 fprt2)
+ Remote SQL: SELECT r5.a, r6.b, r6.c FROM (public.fprt1_p1 r5 LEFT JOIN public.fprt2_p1 r6 ON (((r5.a = r6.b)) AND ((r5.b = r6.a)) AND ((r6.a < 10)))) WHERE ((r5.a < 10))
+(7 rows)
SELECT t1.a,t2.b,t2.c FROM fprt1 t1 LEFT JOIN (SELECT * FROM fprt2 WHERE a < 10) t2 ON (t1.a = t2.b and t1.b = t2.a) WHERE t1.a < 10 ORDER BY 1,2,3;
a | b | c
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index c53bf9c8aa3..28991ccb665 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -55,32 +55,32 @@ set jit = off;
set track_io_timing = off;
-- Simple cases
select explain_filter('explain select * from int8_tbl i8');
- explain_filter
----------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N)
+ explain_filter
+-----------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N)
(1 row)
select explain_filter('explain (analyze, buffers off) select * from int8_tbl i8');
- explain_filter
--------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+ explain_filter
+---------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Execution Time: N.N ms
(3 rows)
select explain_filter('explain (analyze, buffers off, verbose) select * from int8_tbl i8');
- explain_filter
---------------------------------------------------------------------------------------------------------
- Seq Scan on public.int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+ explain_filter
+----------------------------------------------------------------------------------------------------------
+ Seq Scan on public.int8_tbl i8 (cost=N.N..N.N rows=N.N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Output: q1, q2
Planning Time: N.N ms
Execution Time: N.N ms
(4 rows)
select explain_filter('explain (analyze, buffers, format text) select * from int8_tbl i8');
- explain_filter
--------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+ explain_filter
+---------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Execution Time: N.N ms
(3 rows)
@@ -98,7 +98,7 @@ select explain_filter('explain (analyze, buffers, format xml) select * from int8
<Alias>i8</Alias> +
<Startup-Cost>N.N</Startup-Cost> +
<Total-Cost>N.N</Total-Cost> +
- <Plan-Rows>N</Plan-Rows> +
+ <Plan-Rows>N.N</Plan-Rows> +
<Plan-Width>N</Plan-Width> +
<Actual-Startup-Time>N.N</Actual-Startup-Time> +
<Actual-Total-Time>N.N</Actual-Total-Time> +
@@ -147,7 +147,7 @@ select explain_filter('explain (analyze, serialize, buffers, format yaml) select
Alias: "i8" +
Startup Cost: N.N +
Total Cost: N.N +
- Plan Rows: N +
+ Plan Rows: N.N +
Plan Width: N +
Actual Startup Time: N.N +
Actual Total Time: N.N +
@@ -195,9 +195,9 @@ select explain_filter('explain (analyze, serialize, buffers, format yaml) select
(1 row)
select explain_filter('explain (buffers, format text) select * from int8_tbl i8');
- explain_filter
----------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N)
+ explain_filter
+-----------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N)
(1 row)
select explain_filter('explain (buffers, format json) select * from int8_tbl i8');
@@ -213,7 +213,7 @@ select explain_filter('explain (buffers, format json) select * from int8_tbl i8'
"Alias": "i8", +
"Startup Cost": N.N, +
"Total Cost": N.N, +
- "Plan Rows": N, +
+ "Plan Rows": N.N, +
"Plan Width": N, +
"Disabled": false, +
"Shared Hit Blocks": N, +
@@ -247,35 +247,35 @@ select explain_filter('explain (buffers, format json) select * from int8_tbl i8'
select explain_filter('explain verbose select sum(unique1) over w, sum(unique2) over (w order by hundred), sum(tenthous) over (w order by hundred) from tenk1 window w as (partition by ten)');
explain_filter
-------------------------------------------------------------------------------------------------------
- WindowAgg (cost=N.N..N.N rows=N width=N)
+ WindowAgg (cost=N.N..N.N rows=N.N width=N)
Output: sum(unique1) OVER w, (sum(unique2) OVER w1), (sum(tenthous) OVER w1), ten, hundred
Window: w AS (PARTITION BY tenk1.ten)
- -> WindowAgg (cost=N.N..N.N rows=N width=N)
+ -> WindowAgg (cost=N.N..N.N rows=N.N width=N)
Output: ten, hundred, unique1, unique2, tenthous, sum(unique2) OVER w1, sum(tenthous) OVER w1
Window: w1 AS (PARTITION BY tenk1.ten ORDER BY tenk1.hundred)
- -> Sort (cost=N.N..N.N rows=N width=N)
+ -> Sort (cost=N.N..N.N rows=N.N width=N)
Output: ten, hundred, unique1, unique2, tenthous
Sort Key: tenk1.ten, tenk1.hundred
- -> Seq Scan on public.tenk1 (cost=N.N..N.N rows=N width=N)
+ -> Seq Scan on public.tenk1 (cost=N.N..N.N rows=N.N width=N)
Output: ten, hundred, unique1, unique2, tenthous
(11 rows)
select explain_filter('explain verbose select sum(unique1) over w1, sum(unique2) over (w1 order by hundred), sum(tenthous) over (w1 order by hundred rows 10 preceding) from tenk1 window w1 as (partition by ten)');
explain_filter
---------------------------------------------------------------------------------------------------------
- WindowAgg (cost=N.N..N.N rows=N width=N)
+ WindowAgg (cost=N.N..N.N rows=N.N width=N)
Output: sum(unique1) OVER w1, (sum(unique2) OVER w2), (sum(tenthous) OVER w3), ten, hundred
Window: w1 AS (PARTITION BY tenk1.ten)
- -> WindowAgg (cost=N.N..N.N rows=N width=N)
+ -> WindowAgg (cost=N.N..N.N rows=N.N width=N)
Output: ten, hundred, unique1, unique2, tenthous, (sum(unique2) OVER w2), sum(tenthous) OVER w3
Window: w3 AS (PARTITION BY tenk1.ten ORDER BY tenk1.hundred ROWS 'N'::bigint PRECEDING)
- -> WindowAgg (cost=N.N..N.N rows=N width=N)
+ -> WindowAgg (cost=N.N..N.N rows=N.N width=N)
Output: ten, hundred, unique1, unique2, tenthous, sum(unique2) OVER w2
Window: w2 AS (PARTITION BY tenk1.ten ORDER BY tenk1.hundred)
- -> Sort (cost=N.N..N.N rows=N width=N)
+ -> Sort (cost=N.N..N.N rows=N.N width=N)
Output: ten, hundred, unique1, unique2, tenthous
Sort Key: tenk1.ten, tenk1.hundred
- -> Seq Scan on public.tenk1 (cost=N.N..N.N rows=N width=N)
+ -> Seq Scan on public.tenk1 (cost=N.N..N.N rows=N.N width=N)
Output: ten, hundred, unique1, unique2, tenthous
(14 rows)
@@ -295,7 +295,7 @@ select explain_filter('explain (analyze, buffers, format json) select * from int
"Alias": "i8", +
"Startup Cost": N.N, +
"Total Cost": N.N, +
- "Plan Rows": N, +
+ "Plan Rows": N.N, +
"Plan Width": N, +
"Actual Startup Time": N.N, +
"Actual Total Time": N.N, +
@@ -368,11 +368,11 @@ select explain_filter_to_json('explain (settings, format json) select * from int
rollback;
-- GENERIC_PLAN option
select explain_filter('explain (generic_plan) select unique1 from tenk1 where thousand = $1');
- explain_filter
----------------------------------------------------------------------------------
- Bitmap Heap Scan on tenk1 (cost=N.N..N.N rows=N width=N)
+ explain_filter
+-----------------------------------------------------------------------------------
+ Bitmap Heap Scan on tenk1 (cost=N.N..N.N rows=N.N width=N)
Recheck Cond: (thousand = $N)
- -> Bitmap Index Scan on tenk1_thous_tenthous (cost=N.N..N.N rows=N width=N)
+ -> Bitmap Index Scan on tenk1_thous_tenthous (cost=N.N..N.N rows=N.N width=N)
Index Cond: (thousand = $N)
(4 rows)
@@ -382,16 +382,16 @@ ERROR: EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together
CONTEXT: PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
-- MEMORY option
select explain_filter('explain (memory) select * from int8_tbl i8');
- explain_filter
----------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N)
+ explain_filter
+-----------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N)
Memory: used=NkB allocated=NkB
(2 rows)
select explain_filter('explain (memory, analyze, buffers off) select * from int8_tbl i8');
- explain_filter
--------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+ explain_filter
+---------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Memory: used=NkB allocated=NkB
Planning Time: N.N ms
Execution Time: N.N ms
@@ -408,7 +408,7 @@ select explain_filter('explain (memory, summary, format yaml) select * from int8
Alias: "i8" +
Startup Cost: N.N +
Total Cost: N.N +
- Plan Rows: N +
+ Plan Rows: N.N +
Plan Width: N +
Disabled: false +
Planning: +
@@ -430,7 +430,7 @@ select explain_filter('explain (memory, analyze, format json) select * from int8
"Alias": "i8", +
"Startup Cost": N.N, +
"Total Cost": N.N, +
- "Plan Rows": N, +
+ "Plan Rows": N.N, +
"Plan Width": N, +
"Actual Startup Time": N.N, +
"Actual Total Time": N.N, +
@@ -472,9 +472,9 @@ select explain_filter('explain (memory, analyze, format json) select * from int8
prepare int8_query as select * from int8_tbl i8;
select explain_filter('explain (memory) execute int8_query');
- explain_filter
----------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N)
+ explain_filter
+-----------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N)
Memory: used=NkB allocated=NkB
(2 rows)
@@ -496,12 +496,12 @@ create table gen_part_2
partition of gen_part for values in (2);
-- should scan gen_part_1_1 and gen_part_1_2, but not gen_part_2
select explain_filter('explain (generic_plan) select key1, key2 from gen_part where key1 = 1 and key2 = $1');
- explain_filter
----------------------------------------------------------------------------
- Append (cost=N.N..N.N rows=N width=N)
- -> Seq Scan on gen_part_1_1 gen_part_1 (cost=N.N..N.N rows=N width=N)
+ explain_filter
+-----------------------------------------------------------------------------
+ Append (cost=N.N..N.N rows=N.N width=N)
+ -> Seq Scan on gen_part_1_1 gen_part_1 (cost=N.N..N.N rows=N.N width=N)
Filter: ((key1 = N) AND (key2 = $N))
- -> Seq Scan on gen_part_1_2 gen_part_2 (cost=N.N..N.N rows=N width=N)
+ -> Seq Scan on gen_part_1_2 gen_part_2 (cost=N.N..N.N rows=N.N width=N)
Filter: ((key1 = N) AND (key2 = $N))
(5 rows)
@@ -561,7 +561,7 @@ select jsonb_pretty(
"Schema": "public", +
"Disabled": false, +
"Node Type": "Seq Scan", +
- "Plan Rows": 0, +
+ "Plan Rows": 0.0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
"Actual Rows": 0.0, +
@@ -608,7 +608,7 @@ select jsonb_pretty(
"tenk1.tenthous" +
], +
"Node Type": "Sort", +
- "Plan Rows": 0, +
+ "Plan Rows": 0.0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
"Actual Rows": 0.0, +
@@ -652,7 +652,7 @@ select jsonb_pretty(
], +
"Disabled": false, +
"Node Type": "Gather Merge", +
- "Plan Rows": 0, +
+ "Plan Rows": 0.0, +
"Plan Width": 0, +
"Total Cost": 0.0, +
"Actual Rows": 0.0, +
@@ -703,7 +703,7 @@ as 'begin return sin($1); end';
select explain_filter('explain (verbose) select * from t1 where pg_temp.mysin(f1) < 0.5');
explain_filter
------------------------------------------------------------
- Seq Scan on pg_temp.t1 (cost=N.N..N.N rows=N width=N)
+ Seq Scan on pg_temp.t1 (cost=N.N..N.N rows=N.N width=N)
Output: f1
Filter: (pg_temp.mysin(t1.f1) < 'N.N'::double precision)
(3 rows)
@@ -711,53 +711,53 @@ select explain_filter('explain (verbose) select * from t1 where pg_temp.mysin(f1
-- Test compute_query_id
set compute_query_id = on;
select explain_filter('explain (verbose) select * from int8_tbl i8');
- explain_filter
-----------------------------------------------------------------
- Seq Scan on public.int8_tbl i8 (cost=N.N..N.N rows=N width=N)
+ explain_filter
+------------------------------------------------------------------
+ Seq Scan on public.int8_tbl i8 (cost=N.N..N.N rows=N.N width=N)
Output: q1, q2
Query Identifier: N
(3 rows)
-- Test compute_query_id with utility statements containing plannable query
select explain_filter('explain (verbose) declare test_cur cursor for select * from int8_tbl');
- explain_filter
--------------------------------------------------------------
- Seq Scan on public.int8_tbl (cost=N.N..N.N rows=N width=N)
+ explain_filter
+---------------------------------------------------------------
+ Seq Scan on public.int8_tbl (cost=N.N..N.N rows=N.N width=N)
Output: q1, q2
Query Identifier: N
(3 rows)
select explain_filter('explain (verbose) create table test_ctas as select 1');
- explain_filter
-----------------------------------------
- Result (cost=N.N..N.N rows=N width=N)
+ explain_filter
+------------------------------------------
+ Result (cost=N.N..N.N rows=N.N width=N)
Output: N
Query Identifier: N
(3 rows)
-- Test SERIALIZE option
select explain_filter('explain (analyze,buffers off,serialize) select * from int8_tbl i8');
- explain_filter
--------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+ explain_filter
+---------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Serialization: time=N.N ms output=NkB format=text
Execution Time: N.N ms
(4 rows)
select explain_filter('explain (analyze,serialize text,buffers,timing off) select * from int8_tbl i8');
- explain_filter
------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual rows=N.N loops=N)
+ explain_filter
+-------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N) (actual rows=N.N loops=N)
Planning Time: N.N ms
Serialization: output=NkB format=text
Execution Time: N.N ms
(4 rows)
select explain_filter('explain (analyze,serialize binary,buffers,timing) select * from int8_tbl i8');
- explain_filter
--------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+ explain_filter
+---------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Serialization: time=N.N ms output=NkB format=binary
Execution Time: N.N ms
@@ -765,9 +765,9 @@ select explain_filter('explain (analyze,serialize binary,buffers,timing) select
-- this tests an edge case where we have no data to return
select explain_filter('explain (analyze,buffers off,serialize) create temp table explain_temp as select * from int8_tbl i8');
- explain_filter
--------------------------------------------------------------------------------------------------
- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N.N loops=N)
+ explain_filter
+---------------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N.N width=N) (actual time=N.N..N.N rows=N.N loops=N)
Planning Time: N.N ms
Serialization: time=N.N ms output=NkB format=text
Execution Time: N.N ms
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 2a8bfba768e..a6657496d58 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -695,10 +695,10 @@ select tableoid::regclass::text as relname, parted_tab.* from parted_tab order b
-- modifies partition key, but no rows will actually be updated
explain update parted_tab set a = 2 where false;
- QUERY PLAN
---------------------------------------------------------
- Update on parted_tab (cost=0.00..0.00 rows=0 width=0)
- -> Result (cost=0.00..0.00 rows=0 width=10)
+ QUERY PLAN
+-----------------------------------------------------------
+ Update on parted_tab (cost=0.00..0.00 rows=0.00 width=0)
+ -> Result (cost=0.00..0.00 rows=0.00 width=10)
One-Time Filter: false
(3 rows)
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 14da5708451..25ae9f4e714 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2422,8 +2422,8 @@ from int4_tbl t1, int4_tbl t2
left join int4_tbl t3 on t3.f1 > 0
left join int4_tbl t4 on t3.f1 > 1
where t4.f1 is null;
- QUERY PLAN
--------------------------------------------------------
+ QUERY PLAN
+-------------------------------------------------
Nested Loop
-> Nested Loop Left Join
Filter: (t4.f1 IS NULL)
@@ -2433,10 +2433,9 @@ where t4.f1 is null;
Join Filter: (t3.f1 > 1)
-> Seq Scan on int4_tbl t3
Filter: (f1 > 0)
- -> Materialize
- -> Seq Scan on int4_tbl t4
+ -> Seq Scan on int4_tbl t4
-> Seq Scan on int4_tbl t1
-(12 rows)
+(11 rows)
select t1.f1
from int4_tbl t1, int4_tbl t2
@@ -2573,11 +2572,10 @@ where t1.f1 = coalesce(t2.f1, 1);
-> Materialize
-> Seq Scan on int4_tbl t2
Filter: (f1 > 1)
- -> Materialize
- -> Seq Scan on int4_tbl t3
+ -> Seq Scan on int4_tbl t3
-> Materialize
-> Seq Scan on int4_tbl t4
-(14 rows)
+(13 rows)
explain (costs off)
select * from int4_tbl t1
@@ -3740,10 +3738,9 @@ where i41.f1 > 0;
-> Seq Scan on int8_tbl i81
-> Materialize
-> Seq Scan on int4_tbl i42
- -> Materialize
- -> Seq Scan on int4_tbl i43
- Filter: (f1 > 1)
-(12 rows)
+ -> Seq Scan on int4_tbl i43
+ Filter: (f1 > 1)
+(11 rows)
select * from
int4_tbl as i41,
@@ -4332,6 +4329,13 @@ select * from tenk1 a join tenk1 b on
-------------------------------------------------------------------------------------------------
Nested Loop
Join Filter: (((a.unique1 = 1) AND (b.unique1 = 2)) OR ((a.unique2 = 3) AND (b.hundred = 4)))
+ -> Bitmap Heap Scan on tenk1 a
+ Recheck Cond: ((unique1 = 1) OR (unique2 = 3))
+ -> BitmapOr
+ -> Bitmap Index Scan on tenk1_unique1
+ Index Cond: (unique1 = 1)
+ -> Bitmap Index Scan on tenk1_unique2
+ Index Cond: (unique2 = 3)
-> Bitmap Heap Scan on tenk1 b
Recheck Cond: ((unique1 = 2) OR (hundred = 4))
-> BitmapOr
@@ -4339,15 +4343,7 @@ select * from tenk1 a join tenk1 b on
Index Cond: (unique1 = 2)
-> Bitmap Index Scan on tenk1_hundred
Index Cond: (hundred = 4)
- -> Materialize
- -> Bitmap Heap Scan on tenk1 a
- Recheck Cond: ((unique1 = 1) OR (unique2 = 3))
- -> BitmapOr
- -> Bitmap Index Scan on tenk1_unique1
- Index Cond: (unique1 = 1)
- -> Bitmap Index Scan on tenk1_unique2
- Index Cond: (unique2 = 3)
-(17 rows)
+(16 rows)
explain (costs off)
select * from tenk1 a join tenk1 b on
@@ -4356,17 +4352,16 @@ select * from tenk1 a join tenk1 b on
---------------------------------------------------------------------------------------------
Nested Loop
Join Filter: (((a.unique1 = 1) AND (b.unique1 = 2)) OR ((a.unique2 = 3) AND (b.ten = 4)))
+ -> Bitmap Heap Scan on tenk1 a
+ Recheck Cond: ((unique1 = 1) OR (unique2 = 3))
+ -> BitmapOr
+ -> Bitmap Index Scan on tenk1_unique1
+ Index Cond: (unique1 = 1)
+ -> Bitmap Index Scan on tenk1_unique2
+ Index Cond: (unique2 = 3)
-> Seq Scan on tenk1 b
Filter: ((unique1 = 2) OR (ten = 4))
- -> Materialize
- -> Bitmap Heap Scan on tenk1 a
- Recheck Cond: ((unique1 = 1) OR (unique2 = 3))
- -> BitmapOr
- -> Bitmap Index Scan on tenk1_unique1
- Index Cond: (unique1 = 1)
- -> Bitmap Index Scan on tenk1_unique2
- Index Cond: (unique2 = 3)
-(12 rows)
+(11 rows)
explain (costs off)
select * from tenk1 a join tenk1 b on
@@ -5110,13 +5105,12 @@ select 1 from
Join Filter: (i4.f1 IS NOT NULL)
-> Seq Scan on int4_tbl i4
Filter: (2 < f1)
- -> Materialize
- -> Seq Scan on int8_tbl i8
+ -> Seq Scan on int8_tbl i8
-> Result
One-Time Filter: false
-> Materialize
-> Seq Scan on int4_tbl i42
-(16 rows)
+(15 rows)
--
-- test for appropriate join order in the presence of lateral references
@@ -6157,18 +6151,17 @@ FROM int4_tbl
JOIN ((SELECT 42 AS x FROM int8_tbl LEFT JOIN innertab ON q1 = id) AS ss1
RIGHT JOIN tenk1 ON NULL)
ON tenk1.unique1 = ss1.x OR tenk1.unique2 = ss1.x;
- QUERY PLAN
---------------------------------------------------------------------------
+ QUERY PLAN
+--------------------------------------------------------------------
Nested Loop
+ -> Nested Loop Left Join
+ Join Filter: NULL::boolean
+ Filter: ((tenk1.unique1 = (42)) OR (tenk1.unique2 = (42)))
+ -> Seq Scan on tenk1
+ -> Result
+ One-Time Filter: false
-> Seq Scan on int4_tbl
- -> Materialize
- -> Nested Loop Left Join
- Join Filter: NULL::boolean
- Filter: ((tenk1.unique1 = (42)) OR (tenk1.unique2 = (42)))
- -> Seq Scan on tenk1
- -> Result
- One-Time Filter: false
-(9 rows)
+(8 rows)
rollback;
-- another join removal bug: we must clean up correctly when removing a PHV
@@ -6566,28 +6559,26 @@ where t1.a = t2.a;
explain (costs off)
select * from sj x join sj y on x.a = y.a
left join int8_tbl z on x.a = z.q1;
- QUERY PLAN
-------------------------------------
+ QUERY PLAN
+---------------------------------
Nested Loop Left Join
Join Filter: (y.a = z.q1)
-> Seq Scan on sj y
Filter: (a IS NOT NULL)
- -> Materialize
- -> Seq Scan on int8_tbl z
-(6 rows)
+ -> Seq Scan on int8_tbl z
+(5 rows)
explain (costs off)
select * from sj x join sj y on x.a = y.a
left join int8_tbl z on y.a = z.q1;
- QUERY PLAN
-------------------------------------
+ QUERY PLAN
+---------------------------------
Nested Loop Left Join
Join Filter: (y.a = z.q1)
-> Seq Scan on sj y
Filter: (a IS NOT NULL)
- -> Materialize
- -> Seq Scan on int8_tbl z
-(6 rows)
+ -> Seq Scan on int8_tbl z
+(5 rows)
explain (costs off)
select * from (
@@ -6614,16 +6605,15 @@ left join (select coalesce(y.q1, 1) from int8_tbl y
right join sj j1 inner join sj j2 on j1.a = j2.a
on true) z
on true;
- QUERY PLAN
-------------------------------------------
+ QUERY PLAN
+---------------------------------------
Nested Loop Left Join
-> Result
-> Nested Loop Left Join
-> Seq Scan on sj j2
Filter: (a IS NOT NULL)
- -> Materialize
- -> Seq Scan on int8_tbl y
-(7 rows)
+ -> Seq Scan on int8_tbl y
+(6 rows)
-- Test that references to the removed rel in lateral subqueries are replaced
-- correctly after join removal
@@ -6980,32 +6970,36 @@ select p.b from sj p join sj q on p.a = q.a group by p.b having sum(p.a) = 1;
explain (verbose, costs off)
select 1 from (select x.* from sj x, sj y where x.a = y.a) q,
lateral generate_series(1, q.a) gs(i);
- QUERY PLAN
-------------------------------------------------------
+ QUERY PLAN
+------------------------------------------------------------
Nested Loop
Output: 1
-> Seq Scan on public.sj y
Output: y.a, y.b, y.c
Filter: (y.a IS NOT NULL)
- -> Function Scan on pg_catalog.generate_series gs
- Output: gs.i
- Function Call: generate_series(1, y.a)
-(8 rows)
+ -> Memoize
+ Cache Key: y.a
+ Cache Mode: binary
+ -> Function Scan on pg_catalog.generate_series gs
+ Function Call: generate_series(1, y.a)
+(10 rows)
explain (verbose, costs off)
select 1 from (select y.* from sj x, sj y where x.a = y.a) q,
lateral generate_series(1, q.a) gs(i);
- QUERY PLAN
-------------------------------------------------------
+ QUERY PLAN
+------------------------------------------------------------
Nested Loop
Output: 1
-> Seq Scan on public.sj y
Output: y.a, y.b, y.c
Filter: (y.a IS NOT NULL)
- -> Function Scan on pg_catalog.generate_series gs
- Output: gs.i
- Function Call: generate_series(1, y.a)
-(8 rows)
+ -> Memoize
+ Cache Key: y.a
+ Cache Mode: binary
+ -> Function Scan on pg_catalog.generate_series gs
+ Function Call: generate_series(1, y.a)
+(10 rows)
-- Test that a non-EC-derived join clause is processed correctly. Use an
-- outer join so that we can't form an EC.
diff --git a/src/test/regress/expected/join_hash.out b/src/test/regress/expected/join_hash.out
index 4fc34a0e72a..262fa71ed8d 100644
--- a/src/test/regress/expected/join_hash.out
+++ b/src/test/regress/expected/join_hash.out
@@ -508,17 +508,18 @@ set local hash_mem_multiplier = 1.0;
set local enable_parallel_hash = on;
explain (costs off)
select count(*) from simple r join extremely_skewed s using (id);
- QUERY PLAN
------------------------------------------------------------------
- Aggregate
+ QUERY PLAN
+-----------------------------------------------------------------------
+ Finalize Aggregate
-> Gather
Workers Planned: 1
- -> Parallel Hash Join
- Hash Cond: (r.id = s.id)
- -> Parallel Seq Scan on simple r
- -> Parallel Hash
- -> Parallel Seq Scan on extremely_skewed s
-(8 rows)
+ -> Partial Aggregate
+ -> Parallel Hash Join
+ Hash Cond: (r.id = s.id)
+ -> Parallel Seq Scan on simple r
+ -> Parallel Hash
+ -> Parallel Seq Scan on extremely_skewed s
+(9 rows)
select count(*) from simple r join extremely_skewed s using (id);
count
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index d3f5d16a672..6289d3e03d1 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -647,27 +647,27 @@ SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g;
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
----------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1)
+ explain_mask_costs
+------------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=30.00 width=N) (actual rows=30.00 loops=1)
(1 row)
-- As above but with generate_series_timestamp
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMP '2024-02-01', TIMESTAMP '2024-03-01', INTERVAL '1 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
----------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1)
+ explain_mask_costs
+------------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=30.00 width=N) (actual rows=30.00 loops=1)
(1 row)
-- As above but with generate_series_timestamptz_at_zone()
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day', 'UTC') g(s);$$,
true, true, false, true);
- explain_mask_costs
----------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1)
+ explain_mask_costs
+------------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=30.00 width=N) (actual rows=30.00 loops=1)
(1 row)
-- Ensure the estimated and actual row counts match when the range isn't
@@ -675,36 +675,36 @@ true, true, false, true);
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '7 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
--------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1)
+ explain_mask_costs
+----------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=5.00 width=N) (actual rows=5.00 loops=1)
(1 row)
-- Ensure the estimates match when step is decreasing
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '-1 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
----------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1)
+ explain_mask_costs
+------------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=30.00 width=N) (actual rows=30.00 loops=1)
(1 row)
-- Ensure an empty range estimates 1 row
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '1 day') g(s);$$,
true, true, false, true);
- explain_mask_costs
--------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
+ explain_mask_costs
+----------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=1.00 width=N) (actual rows=0.00 loops=1)
(1 row)
-- Ensure we get the default row estimate for infinity values
SELECT explain_mask_costs($$
SELECT * FROM generate_series(TIMESTAMPTZ '-infinity', TIMESTAMPTZ 'infinity', INTERVAL '1 day') g(s);$$,
false, true, false, true);
- explain_mask_costs
--------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=1000 width=N)
+ explain_mask_costs
+----------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=1000.00 width=N)
(1 row)
-- Ensure the row estimate behaves correctly when step size is zero.
@@ -719,36 +719,36 @@ ERROR: step size cannot equal zero
SELECT explain_mask_costs($$
SELECT * FROM generate_series(1.0, 25.0) g(s);$$,
true, true, false, true);
- explain_mask_costs
----------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1)
+ explain_mask_costs
+------------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=25.00 width=N) (actual rows=25.00 loops=1)
(1 row)
-- As above but with non-default step
SELECT explain_mask_costs($$
SELECT * FROM generate_series(1.0, 25.0, 2.0) g(s);$$,
true, true, false, true);
- explain_mask_costs
----------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=13 width=N) (actual rows=13.00 loops=1)
+ explain_mask_costs
+------------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=13.00 width=N) (actual rows=13.00 loops=1)
(1 row)
-- Ensure the estimates match when step is decreasing
SELECT explain_mask_costs($$
SELECT * FROM generate_series(25.0, 1.0, -1.0) g(s);$$,
true, true, false, true);
- explain_mask_costs
----------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1)
+ explain_mask_costs
+------------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=25.00 width=N) (actual rows=25.00 loops=1)
(1 row)
-- Ensure an empty range estimates 1 row
SELECT explain_mask_costs($$
SELECT * FROM generate_series(25.0, 1.0, 1.0) g(s);$$,
true, true, false, true);
- explain_mask_costs
--------------------------------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1)
+ explain_mask_costs
+----------------------------------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=1.00 width=N) (actual rows=0.00 loops=1)
(1 row)
-- Ensure we get the default row estimate for error cases (infinity/NaN values
@@ -756,25 +756,25 @@ true, true, false, true);
SELECT explain_mask_costs($$
SELECT * FROM generate_series('-infinity'::NUMERIC, 'infinity'::NUMERIC, 1.0) g(s);$$,
false, true, false, true);
- explain_mask_costs
--------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=1000 width=N)
+ explain_mask_costs
+----------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=1000.00 width=N)
(1 row)
SELECT explain_mask_costs($$
SELECT * FROM generate_series(1.0, 25.0, 'NaN'::NUMERIC) g(s);$$,
false, true, false, true);
- explain_mask_costs
--------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=1000 width=N)
+ explain_mask_costs
+----------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=1000.00 width=N)
(1 row)
SELECT explain_mask_costs($$
SELECT * FROM generate_series(25.0, 2.0, 0.0) g(s);$$,
false, true, false, true);
- explain_mask_costs
--------------------------------------------------------------------
- Function Scan on generate_series g (cost=N..N rows=1000 width=N)
+ explain_mask_costs
+----------------------------------------------------------------------
+ Function Scan on generate_series g (cost=N..N rows=1000.00 width=N)
(1 row)
-- Test functions for control data
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 6101c8c7cf1..fe960db944f 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -28,8 +28,8 @@ ANALYZE prt2;
-- inner join
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = 0 ORDER BY t1.a, t2.b;
- QUERY PLAN
---------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------
Sort
Sort Key: t1.a
-> Append
@@ -45,13 +45,12 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b =
-> Hash
-> Seq Scan on prt1_p2 t1_2
Filter: (b = 0)
- -> Hash Join
- Hash Cond: (t2_3.b = t1_3.a)
- -> Seq Scan on prt2_p3 t2_3
- -> Hash
- -> Seq Scan on prt1_p3 t1_3
- Filter: (b = 0)
-(21 rows)
+ -> Nested Loop
+ -> Seq Scan on prt1_p3 t1_3
+ Filter: (b = 0)
+ -> Index Scan using iprt2_p3_b on prt2_p3 t2_3
+ Index Cond: (b = t1_3.a)
+(20 rows)
SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = 0 ORDER BY t1.a, t2.b;
a | c | b | c
@@ -834,15 +833,14 @@ SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM prt1 t1, prt2 t2, prt1_e t
Index Cond: (((a + b) / 2) = t2_2.b)
-> Nested Loop
Join Filter: (t1_3.a = ((t3_3.a + t3_3.b) / 2))
- -> Hash Join
- Hash Cond: (t2_3.b = t1_3.a)
- -> Seq Scan on prt2_p3 t2_3
- -> Hash
- -> Seq Scan on prt1_p3 t1_3
- Filter: (b = 0)
+ -> Nested Loop
+ -> Seq Scan on prt1_p3 t1_3
+ Filter: (b = 0)
+ -> Index Scan using iprt2_p3_b on prt2_p3 t2_3
+ Index Cond: (b = t1_3.a)
-> Index Scan using iprt1_e_p3_ab2 on prt1_e_p3 t3_3
Index Cond: (((a + b) / 2) = t2_3.b)
-(33 rows)
+(32 rows)
SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM prt1 t1, prt2 t2, prt1_e t3 WHERE t1.a = t2.b AND t1.a = (t3.a + t3.b)/2 AND t1.b = 0 ORDER BY t1.a, t2.b;
a | c | b | c | ?column? | c
@@ -855,8 +853,8 @@ SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM prt1 t1, prt2 t2, prt1_e t
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2 ON t1.a = t2.b) LEFT JOIN prt1_e t3 ON (t1.a = (t3.a + t3.b)/2) WHERE t1.b = 0 ORDER BY t1.a, t2.b, t3.a + t3.b;
- QUERY PLAN
---------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------
Sort
Sort Key: t1.a, t2.b, ((t3.a + t3.b))
-> Append
@@ -880,17 +878,15 @@ SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2
-> Hash
-> Seq Scan on prt1_p2 t1_2
Filter: (b = 0)
- -> Hash Right Join
- Hash Cond: (((t3_3.a + t3_3.b) / 2) = t1_3.a)
- -> Seq Scan on prt1_e_p3 t3_3
- -> Hash
- -> Hash Right Join
- Hash Cond: (t2_3.b = t1_3.a)
- -> Seq Scan on prt2_p3 t2_3
- -> Hash
- -> Seq Scan on prt1_p3 t1_3
- Filter: (b = 0)
-(33 rows)
+ -> Nested Loop Left Join
+ -> Nested Loop Left Join
+ -> Seq Scan on prt1_p3 t1_3
+ Filter: (b = 0)
+ -> Index Scan using iprt2_p3_b on prt2_p3 t2_3
+ Index Cond: (b = t1_3.a)
+ -> Index Scan using iprt1_e_p3_ab2 on prt1_e_p3 t3_3
+ Index Cond: (((a + b) / 2) = t1_3.a)
+(31 rows)
SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2 ON t1.a = t2.b) LEFT JOIN prt1_e t3 ON (t1.a = (t3.a + t3.b)/2) WHERE t1.b = 0 ORDER BY t1.a, t2.b, t3.a + t3.b;
a | c | b | c | ?column? | c
@@ -911,8 +907,8 @@ SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2 ON t1.a = t2.b) RIGHT JOIN prt1_e t3 ON (t1.a = (t3.a + t3.b)/2) WHERE t3.c = 0 ORDER BY t1.a, t2.b, t3.a + t3.b;
- QUERY PLAN
--------------------------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------------
Sort
Sort Key: t1.a, t2.b, ((t3.a + t3.b))
-> Append
@@ -935,15 +931,14 @@ SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2
-> Index Scan using iprt2_p2_b on prt2_p2 t2_2
Index Cond: (b = t1_2.a)
-> Nested Loop Left Join
- -> Hash Right Join
- Hash Cond: (t1_3.a = ((t3_3.a + t3_3.b) / 2))
- -> Seq Scan on prt1_p3 t1_3
- -> Hash
- -> Seq Scan on prt1_e_p3 t3_3
- Filter: (c = 0)
- -> Index Scan using iprt2_p3_b on prt2_p3 t2_3
- Index Cond: (b = t1_3.a)
-(30 rows)
+ -> Seq Scan on prt1_e_p3 t3_3
+ Filter: (c = 0)
+ -> Nested Loop Left Join
+ -> Index Scan using iprt1_p3_a on prt1_p3 t1_3
+ Index Cond: (a = ((t3_3.a + t3_3.b) / 2))
+ -> Index Scan using iprt2_p3_b on prt2_p3 t2_3
+ Index Cond: (b = t1_3.a)
+(29 rows)
SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2 ON t1.a = t2.b) RIGHT JOIN prt1_e t3 ON (t1.a = (t3.a + t3.b)/2) WHERE t3.c = 0 ORDER BY t1.a, t2.b, t3.a + t3.b;
a | c | b | c | ?column? | c
@@ -1749,8 +1744,8 @@ ALTER TABLE prt2 ATTACH PARTITION prt2_p3 DEFAULT;
ANALYZE prt2;
EXPLAIN (COSTS OFF)
SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = 0 ORDER BY t1.a, t2.b;
- QUERY PLAN
---------------------------------------------------
+ QUERY PLAN
+---------------------------------------------------------------
Sort
Sort Key: t1.a
-> Append
@@ -1766,13 +1761,12 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b =
-> Hash
-> Seq Scan on prt1_p2 t1_2
Filter: (b = 0)
- -> Hash Join
- Hash Cond: (t2_3.b = t1_3.a)
- -> Seq Scan on prt2_p3 t2_3
- -> Hash
- -> Seq Scan on prt1_p3 t1_3
- Filter: (b = 0)
-(21 rows)
+ -> Nested Loop
+ -> Seq Scan on prt1_p3 t1_3
+ Filter: (b = 0)
+ -> Index Scan using iprt2_p3_b on prt2_p3 t2_3
+ Index Cond: (b = t1_3.a)
+(20 rows)
-- test default partition behavior for list
ALTER TABLE plt1 DETACH PARTITION plt1_p3;
@@ -5175,20 +5169,18 @@ SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2
Sort
Sort Key: t1.a, t1.b
-> Append
- -> Hash Join
- Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.b = t2_1.b) AND (t1_1.c = t2_1.c))
+ -> Nested Loop
+ Join Filter: ((t1_1.a = t2_1.a) AND (t1_1.b = t2_1.b) AND (t1_1.c = t2_1.c))
-> Seq Scan on alpha_neg_p1 t1_1
Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
- -> Hash
- -> Seq Scan on beta_neg_p1 t2_1
- Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
- -> Hash Join
- Hash Cond: ((t1_2.a = t2_2.a) AND (t1_2.b = t2_2.b) AND (t1_2.c = t2_2.c))
+ -> Seq Scan on beta_neg_p1 t2_1
+ Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
+ -> Nested Loop
+ Join Filter: ((t1_2.a = t2_2.a) AND (t1_2.b = t2_2.b) AND (t1_2.c = t2_2.c))
-> Seq Scan on alpha_neg_p2 t1_2
Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
- -> Hash
- -> Seq Scan on beta_neg_p2 t2_2
- Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
+ -> Seq Scan on beta_neg_p2 t2_2
+ Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
-> Nested Loop
Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.b = t2_3.b) AND (t1_3.c = t2_3.c))
-> Seq Scan on alpha_pos_p2 t1_3
@@ -5201,7 +5193,7 @@ SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2
Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
-> Seq Scan on beta_pos_p3 t2_4
Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
-(29 rows)
+(27 rows)
SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2.b AND t1.c = t2.c) WHERE ((t1.b >= 100 AND t1.b < 110) OR (t1.b >= 200 AND t1.b < 210)) AND ((t2.b >= 100 AND t2.b < 110) OR (t2.b >= 200 AND t2.b < 210)) AND t1.c IN ('0004', '0009') ORDER BY t1.a, t1.b;
a | b | c | a | b | c
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 1aeed8452bd..b3c0cbf0607 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -1461,18 +1461,17 @@ EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal
------------------------------------------------------------------------------
Nested Loop
Join Filter: (l.cid = r.cid)
+ -> Subquery Scan on l
+ Filter: f_leak(l.cnum)
+ -> Hash Join
+ Hash Cond: (r_1.cid = l_1.cid)
+ -> Seq Scan on credit_card r_1
+ -> Hash
+ -> Seq Scan on customer l_1
+ Filter: (name = CURRENT_USER)
-> Seq Scan on credit_usage r
Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date))
- -> Materialize
- -> Subquery Scan on l
- Filter: f_leak(l.cnum)
- -> Hash Join
- Hash Cond: (r_1.cid = l_1.cid)
- -> Seq Scan on credit_card r_1
- -> Hash
- -> Seq Scan on customer l_1
- Filter: (name = CURRENT_USER)
-(13 rows)
+(12 rows)
SELECT * FROM my_credit_card_usage_secure
WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01';
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 686d8c93aa8..5f17271202a 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -18,8 +18,8 @@ begin
loop
if first_row then
first_row := false;
- tmp := regexp_match(ln, 'rows=(\d*) .* rows=(\d*)');
- return query select tmp[1]::int, tmp[2]::int;
+ tmp := regexp_match(ln, 'rows=(\d+\.\d{2}) .* rows=(\d+\.\d{2})');
+ return query select round(tmp[1]::numeric)::int, round(tmp[2]::numeric)::int;
end if;
end loop;
end;
@@ -1345,13 +1345,13 @@ SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE
SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a = 1 OR a = 51) AND b = ''1''');
estimated | actual
-----------+--------
- 99 | 100
+ 100 | 100
(1 row)
SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a = 1 OR a = 51) AND (b = ''1'' OR b = ''2'')');
estimated | actual
-----------+--------
- 99 | 100
+ 100 | 100
(1 row)
SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a = 1 OR a = 2 OR a = 51 OR a = 52) AND (b = ''1'' OR b = ''2'')');
@@ -1687,13 +1687,13 @@ SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE
SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE ((a * 2) = 2 OR (a * 2) = 102) AND upper(b) = ''1''');
estimated | actual
-----------+--------
- 99 | 100
+ 100 | 100
(1 row)
SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE ((a * 2) = 2 OR (a * 2) = 102) AND (upper(b) = ''1'' OR upper(b) = ''2'')');
estimated | actual
-----------+--------
- 99 | 100
+ 100 | 100
(1 row)
SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE ((a * 2) = 2 OR (a * 2) = 4 OR (a * 2) = 102 OR (a * 2) = 104) AND (upper(b) = ''1'' OR upper(b) = ''2'')');
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index b71a6cd089f..f49a7b6a237 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -20,8 +20,8 @@ begin
loop
if first_row then
first_row := false;
- tmp := regexp_match(ln, 'rows=(\d*) .* rows=(\d*)');
- return query select tmp[1]::int, tmp[2]::int;
+ tmp := regexp_match(ln, 'rows=(\d+\.\d{2}) .* rows=(\d+\.\d{2})');
+ return query select round(tmp[1]::numeric)::int, round(tmp[2]::numeric)::int;
end if;
end loop;
end;
--
2.34.1