Waiting for PostgreSQL 18 – Allow EXPLAIN to indicate fractional rows.

On 21st of February 2025, Robert Haas committed patch:

Allow EXPLAIN to indicate fractional rows.
 
When nloops > 1, we now display two digits after the decimal point,
rather than none. This is important because what we print is actually
planstate->instrument->ntuples / nloops, and sometimes what you want
to know is planstate->instrument->ntuples. You can estimate that by
multiplying the displayed row count by the displayed nloops value, but
the fact that the displayed value is rounded makes that inexact. It's
still inexact even if we show these two extra decimal places, but less
so. Perhaps we will agree on a way to further improve this output later,
but for now this seems better than doing nothing.
 
Author: Ibrar Ahmed <ibrar.ahmad@gmail.com>
Author: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Reviewed-by: David G. Johnston <david.g.johnston@gmail.com>
Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
Reviewed-by: Vignesh C <vignesh21@gmail.com>
Reviewed-by: Greg Stark <stark@mit.edu>
Reviewed-by: Naeem Akhter <akhternaeem@gmail.com>
Reviewed-by: Hamid Akhtar <hamid.akhtar@percona.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Andrei Lepikhov <a.lepikhov@postgrespro.ru>
Reviewed-by: Guillaume Lelarge <guillaume@lelarge.info>
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Reviewed-by: Alena Rybakina <a.rybakina@postgrespro.ru>
Discussion: http://postgr.es/m/603c8f070905281830g2e5419c4xad2946d149e21f9d%40mail.gmail.com

and then, 6 days later he also committed:

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. Although 44cbba9a7f51a3888d5087fc94b23614ba2b81f2
seemed to fix the immediate failures, it may still be the case that there
are lower-probability failures elsewhere in the regression tests.
 
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 currently rows is storead
as 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.
 
Author: Robert Haas <robertmhaas@gmail.com>
Author: Ilia Evdokimov <ilya.evdokimov@tantorlabs.com>
Discussion: http://postgr.es/m/CA+TgmoazzVHn8sFOMFAEwoqBTDxKT45D7mvkyeHgqtoD2cn58Q@mail.gmail.com

This is pretty cool.

Before this, explain analyze for very simple query looked like:

=# explain analyze select 1;
                                     QUERY PLAN
------------------------------------------------------------------------------------
 Result  (cost=0.00..0.01 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=1)
 Planning Time: 4.648 ms
 Execution Time: 0.354 ms
(3 rows)

now, it will look like this:

                                      QUERY PLAN
───────────────────────────────────────────────────────────────────────────────────────
 Result  (cost=0.00..0.01 rows=1 width=4) (actual time=0.002..0.002 rows=1.00 loops=1)
 Planning:
   Buffers: shared hit=3
 Planning Time: 0.113 ms
 Execution Time: 0.041 ms
(5 rows)

The difference is that it shows rows=1.00 in the “actual" part, while previously it was rows=1.

Why is it important. Well, in case of one row, it doesn't really matter, but let me show you something interesting. Let's assume we have table with 5 rows. Like this:

=$ select * from data;
 v
───
 0
 0
 0
 0
 0
 1
(6 rows)

And now, let's assume we want to select all rows from there that have v being 1, 2, 3, and 4. Obviously there is only row with v == 1. Now, to make thing work the way I need it, I will have to force Pg to use nested loop, but it's all possible. After some fiddling, here is explain analyze from Pg 17:

=$ explain analyze
SELECT
    w.*, x.*
FROM
    unnest('{1,2,3,4}'::INT4[]) with ordinality as w (q, idx),
    lateral (
        SELECT * FROM data WHERE v = q ORDER BY v desc LIMIT 1
    ) x;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.00..12.97 rows=4 width=16) (actual time=0.014..0.021 rows=1 loops=1)
   ->  Function Scan on unnest w  (cost=0.00..0.04 rows=4 width=12) (actual time=0.006..0.007 rows=4 loops=1)
   ->  Limit  (cost=0.00..3.22 rows=1 width=4) (actual time=0.003..0.003 rows=0 loops=4)
         ->  Seq Scan on data  (cost=0.00..41.88 rows=13 width=4) (actual time=0.002..0.002 rows=0 loops=4)
               Filter: (v = w.q)
               Rows Removed by Filter: 6
 Planning Time: 1.449 ms
 Execution Time: 0.051 ms
(8 rows)

Please note that Seq Scan on data is shown to be repeated 4 times (loops=4) – that's because the array that I unnested had 4 elements. And it shows that this scan returned 0 rows.

This is because this actual rows shows actually an average. But since Pg < 18 couldn't show fractions it showed 0. Which could cause some issues with rowcount calculation.

In Pg 18, the explain looks differently though:

                                                   QUERY PLAN
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Nested Loop  (cost=0.00..12.97 rows=4 width=16) (actual time=0.017..0.025 rows=1.00 loops=1)
   Buffers: shared hit=4
   ->  Function Scan on unnest w  (cost=0.00..0.04 rows=4 width=12) (actual time=0.006..0.007 rows=4.00 loops=1)
   ->  Limit  (cost=0.00..3.22 rows=1 width=4) (actual time=0.003..0.004 rows=0.25 loops=4)
         Buffers: shared hit=4
         ->  Seq Scan on data  (cost=0.00..41.88 rows=13 width=4) (actual time=0.003..0.003 rows=0.25 loops=4)
               Filter: (v = w.q)
               Rows Removed by Filter: 6
               Buffers: shared hit=4
 Planning:
   Buffers: shared hit=4
 Planning Time: 0.142 ms
 Execution Time: 0.044 ms
(13 rows)

Please note that it showed rows=0.25 – which, after multiplying by 4 (because of loops=4) gives correct number of returned rows, across all loops over this table, to be “1".

This is great. Needed small fix to be handled properly on explain.depesz.com, but it's definitely a great thing.

Thanks a lot, to everyone involved.

2 thoughts on “Waiting for PostgreSQL 18 – Allow EXPLAIN to indicate fractional rows.”

  1. You have a typo:
    > The difference is that it shows rows=1.00 in the “actual” part, while previously it was rows=0.

    At the end of the sentence it should be “…rows=1”.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.