Search code examples
postgresqlisolation-levelmvcc

A call to a VOLATILE function breaks atomicity of a SELECT statement


Given (PostgreSQL 15.3):

create table test (
    test_id int2,
    test_val int4,
    primary key (test_id)
);

If these two transactions are run in parallel at read committed isolation level:

-- Transaction 1
insert into test (select 1, 1 from pg_sleep(2));
-- Transaction 2
select coalesce(
    (select test_val from test where test_id = 1),
    (select null::int4 from pg_sleep(5)),
    (select test_val from test where test_id = 1)
);

The second transaction returns null as it should because it is a single statement.

However, if the second transaction is replaced with:

create function select_testval
    (id int2) returns int4
    language sql strict volatile -- stable function will work as previous version
return (select test_val from test where test_id = id);

-- Transaction 2.1
select coalesce(
    (select test_val from test where test_id = 1),
    (select null::int4 from pg_sleep(5)),
    select_testval(1::int2)
);

It returns 1, acting as a series of two statements each seeing a different snapshot of data.


So, my questions are:

  1. Is it a proper behavior for a function call to introduce a separate statement looking at a newer snapshot? If it is, where is it documented?
  2. Apparently, the stable function gets inlined in this case thus remaining a single statement. Is it possible for a volatile function call to get inlined as well under some circumstances thus changing concurrency semantics? If it is, how to control inlining?

Edit 1. Based on the feedback from comments edited the post to pinpoint the actual problem causing confusion. The original questions were rather trivial.

P. S. For context, I discovered this when was trying to find an idiomatic pure-SQL solution to concurrent inserts with unique constraint problem (1, 2), without resorting to plpgsql.


Solution

  • Okay, it seems that I really overlooked the standard documentation:

    STABLE and IMMUTABLE functions use a snapshot established as of the start of the calling query, whereas VOLATILE functions obtain a fresh snapshot at the start of each query they execute.

    From 38.7. Function Volatility Categories

    This behavior has nothing to do with inlining.

    It is worth noting that the snapshot a volatile function obtains is in line with transaction isolation level. So when isolation is repeatable read, the function would not see any fresh data from other transactions.