2

I'm starting to play with PostgreSQL and noticed that sequences never rollback, even on failed INSERT.
I've read that it is as expected to prevent duplicated sequences on concurrent transactions and I found that weird as my database experience is only with GTM where transaction restarts are common and used precisely for this.

So I wanted to test restarts in PGSQL and loaded this in a database:

CREATE SEQUENCE account_id_seq;

CREATE TABLE account
(
  id integer NOT NULL DEFAULT nextval('account_id_seq'),
  title character varying(40) NOT NULL,
  balance integer NOT NULL DEFAULT 0,
  CONSTRAINT account_pkey PRIMARY KEY (id)
);

INSERT INTO account (title) VALUES ('Test Account');

CREATE OR REPLACE FUNCTION mytest() RETURNS integer AS $$
DECLARE
    cc integer;
BEGIN
    cc := balance from account where id=1;

    RAISE NOTICE 'Balance: %', cc;
    perform pg_sleep(3);

    update account set balance = cc+10 where id=1 RETURNING balance INTO cc;

    return cc;
END
$$
LANGUAGE plpgsql;

So, function mytest() will retrieve balance, wait 3 seconds (to let me launch the other process) and then update balance based on the saved variable.

I now launch 2 calls to this function directly from shell:

void$ psql -c "select * from account where id=1"
 id |    title     | balance 
----+--------------+---------
  1 | Test Account |       0
(1 row)

void$ psql -c "select mytest()" & PIDA=$! && psql -c "select mytest()" && wait $PIDA
[1] 3312
NOTICE:  Balance: 0
NOTICE:  Balance: 0
 mytest 
--------
     10
(1 row)

 mytest 
--------
     10
(1 row)

[1]+  Done                    psql -c "select mytest()"
void$ psql -c "select * from account where id=1"
 id |    title     | balance 
----+--------------+---------
  1 | Test Account |      10
(1 row)

I would expect balance to be 20, not 10, as the last transaction to be committed should restart as the "view" of balance from account where id=1 changed during processing...

I've read about transaction isolation in official documentation and it sounds to me that the default read committed should enforce this behavior precisely..
I've also tested changing the isolation level to serializable and then the last transaction committed does throw an exception but I would like to know if there isn't any "transaction restart" functionality (as I described) or if I'm missing something...

Filipe Pina
  • 2,201
  • 23
  • 35

1 Answers1

2

You get a "restart" automatically if you use proper queries with row level locks. To be precise, the transaction is not restarted as a whole, it just waits its turn when trying to lock a row in default transaction isolation READ COMMITTED:

CREATE OR REPLACE FUNCTION mytest()
   RETURNS integer AS
$func$
DECLARE
   cc integer;
BEGIN
   SELECT INTO cc balance FROM account WHERE id = 1 FOR UPDATE;

   RAISE NOTICE 'Balance: %', cc;
   PERFORM pg_sleep(3);

   UPDATE account SET balance = cc+10
   WHERE id = 1
   RETURNING balance
   INTO cc;

   RETURN cc;
END
$func$  LANGUAGE plpgsql;

SELECT ... FOR UPDATE takes a row level lock to announce the claim that this row is going to be updated. The same function trying the same in another transaction will be blocked and wait until the first commits or rolls back - then take the lock itself and build on the updated row so that the result of your experiment will be 20, not 10.

You can have the same much more efficiently with a plain and simple UPDATE query that takes the appropriate FOR UPDATE locks automatically:

UPDATE account
SET    balance = balance + 10
WHERE  id = 1
RETURNING  balance;

These recent questions seem to have run into similar problems. Detailed explanation and links:

Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • Locks are not exactly the same as the behavior I wanted/described, but it was an alternative that I also did not know how to use and works for my case, so accepted as answer, thanks! And I know I could simplify it into one single update but the purpose was to easily test precisely that, to see if it would "restart". – Filipe Pina Mar 31 '15 at 14:19
  • With "restart" I meant that the second transaction would not wait for first one to finish (like the LOCK on the first line), but it would execute and only at COMMIT part it would check that the "original DB view" changed and results would be unexpected, hence restarting from START TRANSACTION point. – Filipe Pina Mar 31 '15 at 14:21
  • Setting transaction isolation level to "serializable" does this check, but instead of restarting it throws an exception. I guess I could have an outter function handle that exception in a way that it would re-execute the inner function until it didn't throw it, but for the time being LOCKs work and even sound like a better suited solution for my scenario. – Filipe Pina Mar 31 '15 at 14:23
  • Serializable isolation for future reference in case anyone ends up here http://www.postgresql.org/docs/9.3/static/transaction-iso.html#XACT-SERIALIZABLE – Filipe Pina Mar 31 '15 at 14:31
  • Follow up question on http://stackoverflow.com/questions/29372202/postgresql-generic-handler-for-serialization-failure to focus on the serializable question :) – Filipe Pina Mar 31 '15 at 15:18
  • 2
    Strictly, PostgreSQL doesn't restart the query like Oracle (for example) does when it hits a lock in `READ COMMITTED`. Instead it *re-reads just the locked row* and carries on. See `EvalPlanQual` in the source code for some of the gory details. So you can actually get anomalies that wouldn't be possible with a restart-based approach. – Craig Ringer Apr 01 '15 at 01:13