30

Hopefully a simple question, but one for which I haven't readily found a decent answer. I'm reliably informed that stored procedures (user-defined DB functions) in PostgreSQL (specifically, version 9.0.4) are inherently transactional, inasmuch as they are called through a SELECT statement which itself is a transaction. So how does one choose the isolation level of the stored procedure? I believe in other DBMSs the desired transactional block would be wrapped in a START TRANSACTION block for which the desired isolation level is an optional parameter.

As a specific made-up example, say I want to do this:

CREATE FUNCTION add_new_row(rowtext TEXT)
RETURNS VOID AS 
$$
BEGIN
        INSERT INTO data_table VALUES (rowtext);
        UPDATE row_counts_table SET count=count+1;
END;
$$  
LANGUAGE plpgsql
SECURITY DEFINER;

And imagine I want to make sure this function is always performed as a serializable transaction (yes, yes, PostgreSQL SERIALIZABLE isn't proper serializable, but that's not the point). I don't want to require it to be called as

START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT add_new_row('foo');
COMMIT;

So how do I push the required isolation level down into the function? I believe I cannot just put the isolation level in the BEGIN statement, as the manual says

It is important not to confuse the use of BEGIN/END for grouping statements in PL/pgSQL with the similarly-named SQL commands for transaction control. PL/pgSQL's BEGIN/END are only for grouping; they do not start or end a transaction. Functions and trigger procedures are always executed within a transaction established by an outer query — they cannot start or commit that transaction, since there would be no context for them to execute in.

The most obvious approach to me would be to use SET TRANSACTION somewhere in the function definition, e.g.,:

CREATE FUNCTION add_new_row(rowtext TEXT)
RETURNS VOID AS 
$$
BEGIN
        SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
        INSERT INTO data_table VALUES (rowtext);
        UPDATE row_counts_table SET count=count+1;
END;
$$  
LANGUAGE plpgsql
SECURITY DEFINER;

While this would be accepted, it's not clear than I can rely on this to work. The documentation for SET TRANSACTION says

If SET TRANSACTION is executed without a prior START TRANSACTION or BEGIN, it will appear to have no effect, since the transaction will immediately end.

Which leaves me puzzled, since if I call a solitary SELECT add_new_row('foo'); statement I would expect (provided I haven't disabled autocommit) the SELECT to be running as a single-line transaction with the session default isolation level.

The manual also says:

The transaction isolation level cannot be changed after the first query or data-modification statement (SELECT, INSERT, DELETE, UPDATE, FETCH, or COPY) of a transaction has been executed.

So what happens if the function is called from within a transaction with a lower isolation level, e.g.,:

START TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE row_counts_table SET count=0;
SELECT add_new_row('foo');
COMMIT;

For a bonus question: does the language of the function make any difference? Would one set the isolation level differently in PL/pgSQL than in plain SQL?

I'm a fan of standards and documented best practices, so any decent references would be appreciated.

beldaz
  • 4,299
  • 3
  • 43
  • 63
  • 1
    What happened when you tried to use `SET TRANSACTION` inside the function? –  Jun 08 '11 at 06:34
  • @a_horse_with_no_name: As I mentioned, I figure `SET TRANSACTION` is what I need, and functions with it in are accepted, but sometimes that doesn't mean much (some options just get swallowed sometimes), so I'm looking for a documented approach rather than something that just seems to work. – beldaz Jun 08 '11 at 10:34
  • 2
    Postgres seldom swallows what you tell it to do -- and when it does I'd expect it to issue a warning. – Denis de Bernardy Jun 08 '11 at 10:44
  • if the `SET TRANSACTION` inside the function compiles without errors and runs without errors it will not be "swallowed" - unless you are running with autocommit enabled that is. But then you don't have transactions anyway. –  Jun 08 '11 at 11:10
  • @Denis, @a_horse_with_no_name: I have experienced sps being accepted by postgres where options set in the create function statement have not appeared in the stored definition. Even so, whether or not a `SET TRANSACTION` statement is accepted does not mean it does what I want. I will expand my question to clarify. – beldaz Jun 08 '11 at 11:29

4 Answers4

23

You can't do that.

What you could do is have your function check what the current transaction isolation level is and abort if it's not the one you want. You can do this by running SELECT current_setting('transaction_isolation') and then checking the result.

Peter Eisentraut
  • 35,221
  • 12
  • 85
  • 90
  • Yes, I was rather coming round to thinking that was going to be the only approach. I think your "anti-answer" is the the closest thing to what I want ;) – beldaz Jun 09 '11 at 07:30
  • I have checked with the select provided it's 'read committed' but as I'm using postgres_fdw I want it in REPEATABLE READ. So, is there any provision of changing to REPEATABLE READ using functions for frequent data refresh – UmaShankar Jul 08 '19 at 13:53
1

The language of the function makes no difference whatsoever.

This fails:

test=# create function test() returns int as $$
  set transaction isolation level serializable;
  select 1;
$$ language sql;
CREATE FUNCTION
test=# select test();
ERROR:  SET TRANSACTION ISOLATION LEVEL must be called before any query
CONTEXT:  SQL function "test" statement 1

Note that in your particular example, you could do this using a trigger on your first table. Just make sure that row count updates are done in a consistent order to avoid dead-locks, and you'll do fine in repeatable-read mode.

I'm a fan of standards

The PL/languages are platform specific.

Denis de Bernardy
  • 75,850
  • 13
  • 131
  • 154
  • Would you mind expanding on your answer by showing where this should be set? As I mentioned, the example was entirely made up, so I didn't mean to mislead you into thinking I was after a different approach with triggers. As for PL languages being platform specific, they can still have documentation. I just find postgres documentation hard to dip into for a proper answer to something specific, so indications of where exactly to look would be useful to me any other readers. – beldaz Jun 08 '11 at 10:38
  • Just include it in the function's body... Replace `begin` with `begin isolation level serializable` (or add an extra begin/end block, if it coughs an error). – Denis de Bernardy Jun 08 '11 at 10:41
  • 1
    @Denis: replacing `begin` as you suggest would seem to conflict with postgres v9 manual 39.2 - "PL/pgSQL's BEGIN/END are only for grouping; they do not start or end a transaction". – beldaz Jun 08 '11 at 11:01
  • @Denis: Thanks. So does this mean there's no way to do it? http://postgresql.1045698.n5.nabble.com/Changing-the-transaction-isolation-level-within-the-stored-procedure-td2144791.html suggests that may be the case. – beldaz Jun 08 '11 at 12:03
0

Transaction isolation means which changes made in other concurent transactions you can access.

If you want to serialize execution you have to use locks.

You may use after row trigger and update count. "UPDATE row_counts_table" will lock table and all transactions will be serialized. It is slow.

In your example you have two statements. Insert is executed but update have to wait other transactions and count is not valid in this period.

jordani
  • 436
  • 3
  • 3
  • 1
    10 years ago, perhaps. But if you meant in general, then no, serialization does not have to use locks. The original SQL standard assumed that locks would be used, which is why the isolation levels are based on which lock-specific anomalies one is prepared to accept. However, the MVCC approach by postgres and SQL Server is fundamentally different to locking, and though there is the issue of write skew, methods have been devised (e.g., by Fekete et al) to avoid this and so provide serializable execution. – beldaz Jun 09 '11 at 07:38
0

In PG your procedures aren't separate transactions. That is the stored procedure takes part in an existing transaction.

BEGIN TRAN

SELECT 1;
SELECT my_proc(99);

ROLLBACK TRAN;

With that said you have to set the transaction level where the transaction starts which is outside the stored procedure.

One option would be to configure the server to run in the isolation you mostly want to use and do a SET for the edge cases where it differs from your server setting.

Kuberchaun
  • 29,160
  • 7
  • 51
  • 59
  • Alas, that defeats the benefit of stored procedures for me, in that I want some operations to happen together in a server-side unit with a specific transaction isolation level. Think of crediting accounts with interest, for which you don't want to use the intermittent current account value if some other transaction was performing a balance transfer. It would be a shame if one had to resort to client-side transactions for this, and hoping the session default transaction level was right isn't exactly fool-proof. – beldaz Jun 09 '11 at 07:45