Thread: Interesting case of IMMUTABLE significantly hurting performance

Interesting case of IMMUTABLE significantly hurting performance

From
Olleg Samoylov
Date:
PostgreSQL 17.4

CREATE OR REPLACE FUNCTION formatted_num_immutable(p_summa bigint)
  RETURNS text
  LANGUAGE sql
  IMMUTABLE STRICT
RETURN ltrim(to_char(p_summa, '999 999 999 999 999 999 999 999'));

CREATE OR REPLACE FUNCTION formatted_num_stable(p_summa bigint)
  RETURNS text
  LANGUAGE sql
  STABLE STRICT
RETURN ltrim(to_char(p_summa, '999 999 999 999 999 999 999 999'));

explain analyze select formatted_num_immutable(i) from 
generate_series(1,1000000) as i(i);
                                                               QUERY PLAN

--------------------------------------------------------------------------------------------------------------------------------------
  Function Scan on generate_series i  (cost=0.00..262500.00 rows=1000000 
width=32) (actual time=56.892..1548.656 rows=1000000 loops=1)
  Planning Time: 0.039 ms
  JIT:
    Functions: 4
    Options: Inlining false, Optimization false, Expressions true, 
Deforming true
    Timing: Generation 0.184 ms (Deform 0.040 ms), Inlining 0.000 ms, 
Optimization 0.115 ms, Emission 1.570 ms, Total 1.869 ms
  Execution Time: 1587.741 ms
(7 rows)

explain analyze select formatted_num_stable(i) from 
generate_series(1,1000000) as i(i);
                                                              QUERY PLAN

------------------------------------------------------------------------------------------------------------------------------------
  Function Scan on generate_series i  (cost=0.00..17500.00 rows=1000000 
width=32) (actual time=54.993..573.333 rows=1000000 loops=1)
  Planning Time: 0.056 ms
  Execution Time: 598.190 ms
(3 rows)

First interesting thing is immutable variant has cost in 15 time more, 
then stable. That's why jit compilation is tuned on. Second, immutable 
function is working much longer (3 times). And jit is not the reason.

=> set jit=off;
SET
=> explain analyze select formatted_num_immutable(i) from 
generate_series(1,1000000) as i(i);
                                                               QUERY PLAN

--------------------------------------------------------------------------------------------------------------------------------------
  Function Scan on generate_series i  (cost=0.00..262500.00 rows=1000000 
width=32) (actual time=54.888..1537.602 rows=1000000 loops=1)
  Planning Time: 0.052 ms
  Execution Time: 1575.985 ms
(3 rows)

-- 
Olleg




Re: Interesting case of IMMUTABLE significantly hurting performance

From
Laurenz Albe
Date:
On Wed, 2025-04-09 at 19:12 +0300, Olleg Samoylov wrote:
> PostgreSQL 17.4
>
> CREATE OR REPLACE FUNCTION formatted_num_immutable(p_summa bigint)
>   RETURNS text
>   LANGUAGE sql
>   IMMUTABLE STRICT
> RETURN ltrim(to_char(p_summa, '999 999 999 999 999 999 999 999'));
>
> CREATE OR REPLACE FUNCTION formatted_num_stable(p_summa bigint)
>   RETURNS text
>   LANGUAGE sql
>   STABLE STRICT
> RETURN ltrim(to_char(p_summa, '999 999 999 999 999 999 999 999'));
>
> explain analyze select formatted_num_immutable(i) from
> generate_series(1,1000000) as i(i);
>                                                                QUERY PLAN
>
--------------------------------------------------------------------------------------------------------------------------------------
>   Function Scan on generate_series i  (cost=0.00..262500.00 rows=1000000
> width=32) (actual time=56.892..1548.656 rows=1000000 loops=1)
>   Planning Time: 0.039 ms
>   JIT:
>     Functions: 4
>     Options: Inlining false, Optimization false, Expressions true,
> Deforming true
>     Timing: Generation 0.184 ms (Deform 0.040 ms), Inlining 0.000 ms,
> Optimization 0.115 ms, Emission 1.570 ms, Total 1.869 ms
>   Execution Time: 1587.741 ms
> (7 rows)
>
> explain analyze select formatted_num_stable(i) from
> generate_series(1,1000000) as i(i);
>                                                               QUERY PLAN
>
------------------------------------------------------------------------------------------------------------------------------------
>   Function Scan on generate_series i  (cost=0.00..17500.00 rows=1000000
> width=32) (actual time=54.993..573.333 rows=1000000 loops=1)
>   Planning Time: 0.056 ms
>   Execution Time: 598.190 ms
> (3 rows)
>
> First interesting thing is immutable variant has cost in 15 time more,
> then stable. That's why jit compilation is tuned on. Second, immutable
> function is working much longer (3 times). And jit is not the reason.
>
> => set jit=off;
> SET
> => explain analyze select formatted_num_immutable(i) from
> generate_series(1,1000000) as i(i);
>                                                                QUERY PLAN
>
--------------------------------------------------------------------------------------------------------------------------------------
>   Function Scan on generate_series i  (cost=0.00..262500.00 rows=1000000
> width=32) (actual time=54.888..1537.602 rows=1000000 loops=1)
>   Planning Time: 0.052 ms
>   Execution Time: 1575.985 ms
> (3 rows)

If you use EXPLAIN (VERBOSE), you will see that the function gets inlined in the fast case.

That saves the overhead of a function call.

The IMMUTABLE function cannot be inlined because to_char() is not IMMUTABLE.

Yours,
Laurenz Albe



Re: Interesting case of IMMUTABLE significantly hurting performance

From
"David G. Johnston"
Date:
On Wed, Apr 9, 2025 at 1:50 PM Laurenz Albe <laurenz.albe@cybertec.at> wrote:
If you use EXPLAIN (VERBOSE), you will see that the function gets inlined in the fast case.

That saves the overhead of a function call.

The IMMUTABLE function cannot be inlined because to_char() is not IMMUTABLE.

So, the punishment for lying about the volatility of one's function is to prohibit it from being inlined even in a case where had you been truthful about the volatility it would have been inlined.

David J.

Re: Interesting case of IMMUTABLE significantly hurting performance

From
Nico Williams
Date:
On Wed, Apr 09, 2025 at 10:50:00PM +0200, Laurenz Albe wrote:
> The IMMUTABLE function cannot be inlined because to_char() is not IMMUTABLE.

Q: Why would to_char() not be IMMUTABLE?

A: Because it makes use of locales, and I guess the guc-timezone GUC,
   which could change if the expression is ultimately used in a PlPgSQL
   fragment, or if it's in a prepared statement.  (I think.)

That to_char is not immutable is not documented though.  Though it's
clear when looking at the docs for the `jsonb_.*_tz()` functions.

Nico
-- 



Re: Interesting case of IMMUTABLE significantly hurting performance

From
Adrian Klaver
Date:
On 4/9/25 14:21, Nico Williams wrote:
> On Wed, Apr 09, 2025 at 10:50:00PM +0200, Laurenz Albe wrote:
>> The IMMUTABLE function cannot be inlined because to_char() is not IMMUTABLE.
> 
> Q: Why would to_char() not be IMMUTABLE?
> 
> A: Because it makes use of locales, and I guess the guc-timezone GUC,
>     which could change if the expression is ultimately used in a PlPgSQL
>     fragment, or if it's in a prepared statement.  (I think.)
> 
> That to_char is not immutable is not documented though.  Though it's
> clear when looking at the docs for the `jsonb_.*_tz()` functions.

 From here:

https://www.postgresql.org/docs/current/catalog-pg-proc.html

select proname, provolatile, prosrc  from pg_proc where proname='to_char';
  proname | provolatile |       prosrc
---------+-------------+---------------------
  to_char | s           | timestamptz_to_char
  to_char | s           | numeric_to_char
  to_char | s           | int4_to_char
  to_char | s           | int8_to_char
  to_char | s           | float4_to_char
  to_char | s           | float8_to_char
  to_char | s           | interval_to_char
  to_char | s           | timestamp_to_char

Where 's' is:

"It is s for “stable” functions, whose results (for fixed inputs) do not 
change within a scan."

> 
> Nico

-- 
Adrian Klaver
adrian.klaver@aklaver.com




"David G. Johnston" <david.g.johnston@gmail.com> writes:
> On Wed, Apr 9, 2025 at 1:50 PM Laurenz Albe <laurenz.albe@cybertec.at>
> wrote:
>> The IMMUTABLE function cannot be inlined because to_char() is not
>> IMMUTABLE.

> So, the punishment for lying about the volatility of one's function is to
> prohibit it from being inlined even in a case where had you been truthful
> about the volatility it would have been inlined.

Yeah.  The assumption is that you had a reason for marking the
function IMMUTABLE and you want the planner to treat it that way
even if it isn't really.  (There are valid use-cases for that, for
instance if you want calls to the function to be constant-folded.)

So we don't inline the function --- if we did, the merely-stable
contained expression would be exposed and then treated as STABLE.
But that comes at a pretty substantial cost, since the
SQL-language-function executor isn't exactly free.

If you err in the other direction, you don't get slapped on the
wrist that way.  We're willing to inline VOLATILE functions,
for instance, whether or not the contained expression is volatile.
Similarly for STRICT, and I think parallel safety as well.
So my own habit when writing a SQL function that I wish to be
inlined is to leave off all those markings.  They won't matter
if the function is successfully inlined, and they might get in
the way of that happening.

            regards, tom lane



Re: Interesting case of IMMUTABLE significantly hurting performance

From
Olleg Samoylov
Date:

On 10.04.2025 01:08, Tom Lane wrote:
> Yeah.  The assumption is that you had a reason for marking the
> function IMMUTABLE and you want the planner to treat it that way
> even if it isn't really.  (There are valid use-cases for that, for
> instance if you want calls to the function to be constant-folded.)
>             regards, tom lane

Well, to_char(bigint, text) indeed not immutable, because in some 
pattern it uses get information from locale. For instance,'SLDG' 
patterns. But in case of

CREATE OR REPLACE FUNCTION formatted_num_immutable(p_summa bigint)
  RETURNS text
  LANGUAGE sql
  IMMUTABLE STRICT
RETURN ltrim(to_char(p_summa, '999 999 999 999 999 999 999 999'));

to_char do not use locale information in this pattern. So it is correct 
conclude that to_char is immutable with this pattern and 
formatted_num_immutable too. I did not lie to the planner.

So this is looked "strange", immutable function marked as immutable 
function can not be inlined, but exactly the same function marked as 
volatile do.
-- 
Olleg




Re: Interesting case of IMMUTABLE significantly hurting performance

From
"David G. Johnston"
Date:
On Wednesday, April 9, 2025, Olleg Samoylov <splarv@ya.ru> wrote:
On 10.04.2025 01:08, Tom Lane wrote:
Yeah.  The assumption is that you had a reason for marking the
function IMMUTABLE and you want the planner to treat it that way
even if it isn't really.  (There are valid use-cases for that, for
instance if you want calls to the function to be constant-folded.)
                        regards, tom lane

Well, to_char(bigint, text) indeed not immutable, because in some pattern it uses get information from locale. For instance,'SLDG' patterns. But in case of

CREATE OR REPLACE FUNCTION formatted_num_immutable(p_summa bigint)
 RETURNS text
 LANGUAGE sql
 IMMUTABLE STRICT
RETURN ltrim(to_char(p_summa, '999 999 999 999 999 999 999 999'));

to_char do not use locale information in this pattern. So it is correct conclude that to_char is immutable with this pattern and formatted_num_immutable too. I did not lie to the planner.

So this is looked "strange", immutable function marked as immutable function can not be inlined, but exactly the same function marked as volatile do.


Yeah, the inlining is an optimization, and while it seems like it could perform more tests or maybe make slightly different/more adjustments, it really isn’t worth the development effort or runtime cost to do so.  Make your function volatility match the most volatile function you internally call - constant input arguments don’t change this.

There is no reason to perform number formatting immutably - function call results involving table data are not memoized.

David J.

Re: Interesting case of IMMUTABLE significantly hurting performance

From
Adrian Klaver
Date:
On 4/9/25 22:33, Olleg Samoylov wrote:
> 
> 
> On 10.04.2025 01:08, Tom Lane wrote:
>> Yeah.  The assumption is that you had a reason for marking the
>> function IMMUTABLE and you want the planner to treat it that way
>> even if it isn't really.  (There are valid use-cases for that, for
>> instance if you want calls to the function to be constant-folded.)
>>             regards, tom lane
> 
> Well, to_char(bigint, text) indeed not immutable, because in some 
> pattern it uses get information from locale. For instance,'SLDG' 
> patterns. But in case of
> 
> CREATE OR REPLACE FUNCTION formatted_num_immutable(p_summa bigint)
>   RETURNS text
>   LANGUAGE sql
>   IMMUTABLE STRICT
> RETURN ltrim(to_char(p_summa, '999 999 999 999 999 999 999 999'));
> 
> to_char do not use locale information in this pattern. So it is correct 
> conclude that to_char is immutable with this pattern and 
> formatted_num_immutable too. I did not lie to the planner.

I doubt the code actually looks at the template, it just does the lookup 
on pg_proc and sees:

proname | provolatile |       prosrc
---------+-------------+---------------------

to_char | s           | int8_to_char

> 
> So this is looked "strange", immutable function marked as immutable 
> function can not be inlined, but exactly the same function marked as 
> volatile do.

-- 
Adrian Klaver
adrian.klaver@aklaver.com




Re: Interesting case of IMMUTABLE significantly hurting performance

From
Nico Williams
Date:
On Wed, Apr 09, 2025 at 02:43:11PM -0700, Adrian Klaver wrote:
> On 4/9/25 14:21, Nico Williams wrote:
> > That to_char is not immutable is not documented though.  Though it's
> > clear when looking at the docs for the `jsonb_.*_tz()` functions.
> 
> From here:
> 
> https://www.postgresql.org/docs/current/catalog-pg-proc.html
> 
> select proname, provolatile, prosrc  from pg_proc where proname='to_char';
> [...]

I'm surprised to see that counted as docs, but good to know.



Re: Interesting case of IMMUTABLE significantly hurting performance

From
"David G. Johnston"
Date:
On Thu, Apr 10, 2025 at 8:49 AM Nico Williams <nico@cryptonector.com> wrote:
On Wed, Apr 09, 2025 at 02:43:11PM -0700, Adrian Klaver wrote:
> On 4/9/25 14:21, Nico Williams wrote:
> > That to_char is not immutable is not documented though.  Though it's
> > clear when looking at the docs for the `jsonb_.*_tz()` functions.
>
> From here:
>
> https://www.postgresql.org/docs/current/catalog-pg-proc.html
>
> select proname, provolatile, prosrc  from pg_proc where proname='to_char';
> [...]

I'm surprised to see that counted as docs, but good to know.


Consulting pg_proc constitutes, IMO, going outside the documentation to retrieve information.  It is just not high up on anyone's annoyance list to try and get this piece of information incorporated into the documentation.  Partly because \df+ does show this information as well, so at least one doesn't have to go write the catalog query themself.

David J.

Re: Interesting case of IMMUTABLE significantly hurting performance

From
Adrian Klaver
Date:
On 4/10/25 08:48, Nico Williams wrote:
> On Wed, Apr 09, 2025 at 02:43:11PM -0700, Adrian Klaver wrote:
>> On 4/9/25 14:21, Nico Williams wrote:
>>> That to_char is not immutable is not documented though.  Though it's
>>> clear when looking at the docs for the `jsonb_.*_tz()` functions.
>>
>>  From here:
>>
>> https://www.postgresql.org/docs/current/catalog-pg-proc.html
>>
>> select proname, provolatile, prosrc  from pg_proc where proname='to_char';
>> [...]
> 
> I'm surprised to see that counted as docs, but good to know.

When in doubt consult the system catalogs.

My guess is the absence of specific volatility information in the data 
formatting functions is due to that information originating from a time 
before provolatile existed. Whereas the jsonb_.*_tz() functions are 
relatively new and where documented with knowledge of provolatile.


-- 
Adrian Klaver
adrian.klaver@aklaver.com




Re: Interesting case of IMMUTABLE significantly hurting performance

From
Merlin Moncure
Date:
On Thu, Apr 10, 2025 at 10:59 AM David G. Johnston <david.g.johnston@gmail.com> wrote:
On Thu, Apr 10, 2025 at 8:49 AM Nico Williams <nico@cryptonector.com> wrote:
On Wed, Apr 09, 2025 at 02:43:11PM -0700, Adrian Klaver wrote:
> On 4/9/25 14:21, Nico Williams wrote:
> > That to_char is not immutable is not documented though.  Though it's
> > clear when looking at the docs for the `jsonb_.*_tz()` functions.
>
> From here:
>
> https://www.postgresql.org/docs/current/catalog-pg-proc.html
>
> select proname, provolatile, prosrc  from pg_proc where proname='to_char';
> [...]

I'm surprised to see that counted as docs, but good to know.


Consulting pg_proc constitutes, IMO, going outside the documentation to retrieve information.  It is just not high up on anyone's annoyance list to try and get this piece of information incorporated into the documentation.  Partly because \df+ does show this information as well, so at least one doesn't have to go write the catalog query themself.

Facts.  This is black magic.   This has come up over and over. 

I guess the real problems here are lack of feedback on a number of fronts:
*) the server knows the function is not immutable but lets you create it anyway, even though it can have negative downstream consequences
*) there is no way to discern inline vs non-inlined execution in explain
*) the planner is clearly not modelling function scan overhead give the relative costing discrepancies

I think the first point is the most serious. A warning on function creation, 'WARNING: this function is not provably immutable and therefore is not eligible for inlining" with some appropriate hints might do the trick, in the very specific situation where the function is otherwise eligible.  

merlin
Merlin Moncure <mmoncure@gmail.com> writes:
> I guess the real problems here are lack of feedback on a number of fronts:
> *) the server knows the function is not immutable but lets you create it
> anyway, even though it can have negative downstream consequences

That's debatable I think.  If you know what you're doing, you're going
to be annoyed by warnings telling you that you don't.

> *) there is no way to discern inline vs non-inlined execution in explain

That's simply false.  Using the examples in this thread:

regression=# explain (verbose,analyze)
select formatted_num_immutable(i) from generate_series(1,1000000) i;
                                                                     QUERY PLAN
                            

----------------------------------------------------------------------------------------------------------------------------------------------------
 Function Scan on pg_catalog.generate_series i  (cost=0.00..262500.00 rows=1000000 width=32) (actual
time=65.535..2444.956rows=1000000.00 loops=1) 
   Output: formatted_num_immutable((i)::bigint)
   Function Call: generate_series(1, 1000000)
   Buffers: temp read=1709 written=1709
 Planning Time: 0.086 ms
 Execution Time: 2481.218 ms
(6 rows)

regression=# explain (verbose,analyze)
select formatted_num_stable(i) from generate_series(1,1000000) i;
                                                                    QUERY PLAN
                          

--------------------------------------------------------------------------------------------------------------------------------------------------
 Function Scan on pg_catalog.generate_series i  (cost=0.00..17500.00 rows=1000000 width=32) (actual
time=65.615..478.780rows=1000000.00 loops=1) 
   Output: ltrim(to_char((i)::bigint, '999 999 999 999 999 999 999 999'::text))
   Function Call: generate_series(1, 1000000)
   Buffers: temp read=1709 written=1709
 Planning Time: 0.091 ms
 Execution Time: 501.412 ms
(6 rows)

You can easily see that the second case was inlined, because you don't
see the SQL function anymore, rather its body.

> *) the planner is clearly not modelling function scan overhead give the
> relative costing discrepancies

That's also false; note the 15x difference in estimated cost above,
which is actually more than the real difference in runtime.  (I hasten
to add that I don't have a lot of faith in our function cost
estimates.  But the planner is quite well aware that a non-inlined SQL
function is likely to be expensive.)

            regards, tom lane



Re: Interesting case of IMMUTABLE significantly hurting performance

From
"David G. Johnston"
Date:
On Thursday, April 10, 2025, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Merlin Moncure <mmoncure@gmail.com> writes:
> I guess the real problems here are lack of feedback on a number of fronts:
> *) the server knows the function is not immutable but lets you create it
> anyway, even though it can have negative downstream consequences

That's debatable I think.  If you know what you're doing, you're going
to be annoyed by warnings telling you that you don't.

So long as you use atomic SQL functions I suspect it is possible to use the dependency data to get the volatility of the used functions and compare them to the volatility of the UDF.

David J.

Re: Interesting case of IMMUTABLE significantly hurting performance

From
Laurenz Albe
Date:
On Thu, 2025-04-10 at 22:18 -0500, Merlin Moncure wrote:
> Facts.  This is black magic.   This has come up over and over. 

Perhaps it would help to add the excellent information from the Wiki to the documentation:
https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions

Yours,
Laurenz Albe



Re: Interesting case of IMMUTABLE significantly hurting performance

From
Nico Williams
Date:
On Fri, Apr 11, 2025 at 07:15:44AM +0200, Laurenz Albe wrote:
> On Thu, 2025-04-10 at 22:18 -0500, Merlin Moncure wrote:
> > Facts.  This is black magic.   This has come up over and over. 
> 
> Perhaps it would help to add the excellent information from the Wiki to the documentation:
> https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions

Adding a warning in the functions _docs_ to check their various
attributes in the cataclog would suffice.



Re: Interesting case of IMMUTABLE significantly hurting performance

From
Dominique Devienne
Date:
On Fri, Apr 11, 2025 at 5:52 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Merlin Moncure <mmoncure@gmail.com> writes:
> > I guess the real problems here are lack of feedback on a number of fronts:
> > *) the server knows the function is not immutable but lets you create it
> > anyway, even though it can have negative downstream consequences
>
> That's debatable I think.  If you know what you're doing, you're going
> to be annoyed by warnings telling you that you don't.

True, but that's typically what pragmas in the code are for then,
to explicitly suppress warnings.

Or a new option in the CREATE FUNCTION command. Better safe than sorry,
and I prefer useful "on-by-default" warnings for the non-experts,
which can be disabled, than silence.

PS: And FWIW, I removed all attributes from the functions I'm
writting, based on your aerlier advice Tom.



Re: Interesting case of IMMUTABLE significantly hurting performance

From
Merlin Moncure
Date:
On Thu, Apr 10, 2025 at 10:51 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Merlin Moncure <mmoncure@gmail.com> writes:
> I guess the real problems here are lack of feedback on a number of fronts:
> *) the server knows the function is not immutable but lets you create it
> anyway, even though it can have negative downstream consequences

That's debatable I think.  If you know what you're doing, you're going
to be annoyed by warnings telling you that you don't.

> *) there is no way to discern inline vs non-inlined execution in explain

That's simply false.  Using the examples in this thread:

ah -- gotcha.  misread the original email --  should have known better :) 
thanks 

merlin

Re: Interesting case of IMMUTABLE significantly hurting performance

From
Wolfgang Walther
Date:
Tom Lane:
> If you err in the other direction, you don't get slapped on the
> wrist that way.  We're willing to inline VOLATILE functions,
> for instance, whether or not the contained expression is volatile.
> Similarly for STRICT, and I think parallel safety as well.
> So my own habit when writing a SQL function that I wish to be
> inlined is to leave off all those markings.

According to [1], this only applies to inlining of scalar functions, but 
not to table functions, which *need* to be either STABLE or IMMUTABLE.

Just mentioning this for anyone taking this advice blindly and leaving 
all marks off, which might not always work as expected in the general case.

Best,

Wolfgang

[1]: https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions