Thread: Monotonic WindowFunc support for ntile(), percent_rank() and cume_dist()

Monotonic WindowFunc support for ntile(), percent_rank() and cume_dist()

From
David Rowley
Date:
9d9c02ccd [1] added infrastructure in the query planner and executor
so that the executor would know to stop processing a monotonic
WindowFunc when its value went beyond what some qual in the outer
query could possibly match in future evaluations due to the
WindowFunc's monotonic nature.

In that commit, support was added so that the optimisation would work
for row_number(), rank(), dense_rank() and forms of count(*).  On
further inspection, it looks like the same can be done for ntile(),
percent_rank() and cume_dist(). These WindowFuncs are always
monotonically increasing.

I've attached a trivial patch to add the required support request type
to the existing prosupport functions for these window functions.

David

[1] https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=9d9c02ccd

Attachment

Re: Monotonic WindowFunc support for ntile(), percent_rank() and cume_dist()

From
Melanie Plageman
Date:
On Tue, Jan 24, 2023 at 11:01:08AM +1300, David Rowley wrote:
> 9d9c02ccd [1] added infrastructure in the query planner and executor
> so that the executor would know to stop processing a monotonic
> WindowFunc when its value went beyond what some qual in the outer
> query could possibly match in future evaluations due to the
> WindowFunc's monotonic nature.
> 
> In that commit, support was added so that the optimisation would work
> for row_number(), rank(), dense_rank() and forms of count(*).  On
> further inspection, it looks like the same can be done for ntile(),
> percent_rank() and cume_dist(). These WindowFuncs are always
> monotonically increasing.
> 

Silly question, but was there any reason these were omitted in the first
commit?

> diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
> index af13b8e53d..b87a624fb2 100644
> --- a/src/backend/utils/adt/windowfuncs.c
> +++ b/src/backend/utils/adt/windowfuncs.c
> @@ -288,6 +288,15 @@ window_percent_rank_support(PG_FUNCTION_ARGS)
>  {
>      Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
>  
> +    if (IsA(rawreq, SupportRequestWFuncMonotonic))
> +    {
> +        SupportRequestWFuncMonotonic *req = (SupportRequestWFuncMonotonic *) rawreq;
> +
> +        /* percent_rank() is monotonically increasing */
> +        req->monotonic = MONOTONICFUNC_INCREASING;
> +        PG_RETURN_POINTER(req);
> +    }
> +
>      if (IsA(rawreq, SupportRequestOptimizeWindowClause))
>      {
>          SupportRequestOptimizeWindowClause *req = (SupportRequestOptimizeWindowClause *) rawreq;
> @@ -362,6 +371,15 @@ window_cume_dist_support(PG_FUNCTION_ARGS)
>  {
>      Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
>  
> +    if (IsA(rawreq, SupportRequestWFuncMonotonic))
> +    {
> +        SupportRequestWFuncMonotonic *req = (SupportRequestWFuncMonotonic *) rawreq;
> +
> +        /* cume_dist() is monotonically increasing */
> +        req->monotonic = MONOTONICFUNC_INCREASING;
> +        PG_RETURN_POINTER(req);
> +    }
> +
>      if (IsA(rawreq, SupportRequestOptimizeWindowClause))
>      {
>          SupportRequestOptimizeWindowClause *req = (SupportRequestOptimizeWindowClause *) rawreq;
> @@ -465,6 +483,18 @@ window_ntile_support(PG_FUNCTION_ARGS)
>  {
>      Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
>  
> +    if (IsA(rawreq, SupportRequestWFuncMonotonic))
> +    {
> +        SupportRequestWFuncMonotonic *req = (SupportRequestWFuncMonotonic *) rawreq;
> +
> +        /*
> +         * ntile() is monotonically increasing as the number of buckets cannot
> +         * change after the first call
> +         */
> +        req->monotonic = MONOTONICFUNC_INCREASING;
> +        PG_RETURN_POINTER(req);
> +    }
> +
>      if (IsA(rawreq, SupportRequestOptimizeWindowClause))
>      {
>          SupportRequestOptimizeWindowClause *req = (SupportRequestOptimizeWindowClause *) rawreq;

Since all three cases are exactly the same code, maybe you could
macro-ize it and add a single comment?

- Melanie



Re: Monotonic WindowFunc support for ntile(), percent_rank() and cume_dist()

From
David Rowley
Date:
Thanks for having a look at this.

On Tue, 24 Jan 2023 at 13:26, Melanie Plageman
<melanieplageman@gmail.com> wrote:
> Silly question, but was there any reason these were omitted in the first
> commit?

Good question, it's just that I didn't think of it at the time and
nobody else did or if they did, they didn't mention it.

> Since all three cases are exactly the same code, maybe you could
> macro-ize it and add a single comment?

Hmm, I kinda like that it's being spelt out explicitly. To me, it
seems clean and easy to read. I know we could have fewer lines of code
with something else, but for me, being able to quickly see what the
properties of the WindowFunc are without having to look at some other
function is more important than saving some space in windowfuncs.c

I'd likely feel differently if the code in question didn't all fit on
my screen at once, but it does and I can see at a quick glance that
the function is unconditionally monotonically increasing. Functions
such as COUNT(*) are conditionally monotonically
increasing/decreasing, depending on the frame options.

If you feel strongly about that, then feel free to show me what you
have in mind in more detail so I can think harder about it.

David



Re: Monotonic WindowFunc support for ntile(), percent_rank() and cume_dist()

From
Melanie Plageman
Date:
On Tue, Jan 24, 2023 at 02:00:33PM +1300, David Rowley wrote:
> Thanks for having a look at this.
> 
> On Tue, 24 Jan 2023 at 13:26, Melanie Plageman
> <melanieplageman@gmail.com> wrote:
> 
> > Since all three cases are exactly the same code, maybe you could
> > macro-ize it and add a single comment?
> 
> Hmm, I kinda like that it's being spelt out explicitly. To me, it
> seems clean and easy to read. I know we could have fewer lines of code
> with something else, but for me, being able to quickly see what the
> properties of the WindowFunc are without having to look at some other
> function is more important than saving some space in windowfuncs.c
> 
> I'd likely feel differently if the code in question didn't all fit on
> my screen at once, but it does and I can see at a quick glance that
> the function is unconditionally monotonically increasing. Functions
> such as COUNT(*) are conditionally monotonically
> increasing/decreasing, depending on the frame options.
> 
> If you feel strongly about that, then feel free to show me what you
> have in mind in more detail so I can think harder about it.

Nah, I don't feel strongly. I think it was because looking at the patch
in isolation, the repetition stands out but in the context of the rest
of the code it doesn't.

- Melanie



Re: Monotonic WindowFunc support for ntile(), percent_rank() and cume_dist()

From
David Rowley
Date:
On Wed, 25 Jan 2023 at 02:39, Melanie Plageman
<melanieplageman@gmail.com> wrote:
>
> On Tue, Jan 24, 2023 at 02:00:33PM +1300, David Rowley wrote:
> > If you feel strongly about that, then feel free to show me what you
> > have in mind in more detail so I can think harder about it.
>
> Nah, I don't feel strongly. I think it was because looking at the patch
> in isolation, the repetition stands out but in the context of the rest
> of the code it doesn't.

I played around with this patch again and the performance gains for
the best case are not quite as good as we got for row_number(), rank()
and dense_rank() in the original commit.  The reasons for this is that
ntile() and co all require getting a count of the total rows in the
partition so it can calculate the result. ntile() needs to know how
large the tiles are, for example. That obviously requires pulling all
tuples into the tuplestore.

Despite this, the performance with the patch is still much better than
without.  Here's master:

explain (analyze, timing off, costs off) select * from (select
a,ntile(10) over (order by a) nt from a) where nt < 1;

 Subquery Scan on unnamed_subquery (actual rows=0 loops=1)
   Filter: (unnamed_subquery.nt < 1)
   Rows Removed by Filter: 1000000
   ->  WindowAgg  (actual rows=1000000 loops=1)
         ->  Index Only Scan using a_a_idx on a  (actual rows=1000000 loops=1)
               Heap Fetches: 0
 Planning Time: 0.073 ms
 Execution Time: 254.118 ms
(8 rows)

and with the patch:

 WindowAgg  (actual rows=0 loops=1)
   Run Condition: (ntile(10) OVER (?) < 1)
   ->  Index Only Scan using a_a_idx on a (actual rows=1000000 loops=1)
         Heap Fetches: 0
 Planning Time: 0.072 ms
 Execution Time: 140.023 ms
(6 rows)

Here's with row_number() for reference:

explain (analyze, timing off, costs off) select * from (select
a,row_number() over (order by a) rn from a) where rn < 1;

 WindowAgg (actual rows=0 loops=1)
   Run Condition: (row_number() OVER (?) < 1)
   ->  Index Only Scan using a_a_idx on a (actual rows=1 loops=1)
         Heap Fetches: 0
 Planning Time: 0.089 ms
 Execution Time: 0.054 ms
(6 rows)

you can clearly see the difference with the number of rows pulled out
of the index only scan.

This is just a 1 million row table with a single INT column and an
index on that column.

Anyway, all seems like clear wins and low controversy so I've now pushed it.

David