Trying to keep rows consistent then referencing previous rows.
CREATE TABLE transactions (
id int PRIMARY KEY
GENERATED BY DEFAULT AS IDENTITY,
account_id bigint REFERENCES account,
change integer NOT NULL,
balance integer NOT NULL,
)
Query from the client is basically
SELECT balance
FROM transactions
WHERE account_id = X
ORDER BY id DESC
LIMIT 1;
INSERT INTO transactions (account_id, change, balance)
VALUES (X, 100, $balance + 100);
This code will produce incorrect results on concurrent inserts.
So, the question is how to make balance calculation always correct?
Will INSERT INTO ... SELECT(...)
be enough?
Best Answer
It's not safe with this schema at all.
Will execute a query in a single transaction but that transaction does not lock the table you're
SELECT
ing from against futureINSERT
s. The problem here is thatSELECT
itself hasORDER BY id DESC LIMIT 1;
There is no way under this schema to do that without table-locking. The database doesn't understand that insertion-dynamics behindid
(which no database does). Because of this at best you can implicitly lock the single row you get with theSELECT
, but there is no guarantee that that's the newest row by the time you get toINSERT
.So what are the solutions here,
SELECT FROM accounts WHERE account_id = x FOR UPDATE
theFOR UPDATE
acquires a row-level exclusive lock on the row. If this statement is in theINSERT INTO .. SELECT
then futureSELECT FOR UPDATE
s on that row will block and wait. Alternatively it can be anywhere in the same transaction before the read from the transaction table. This is what I would do.SERIALIZABLE
mode, but only because there is anid = x
on theSELECT
. But, to be honest I'm not sure and I don't use that mode because I prefer less voodoo and by extension an aggregate on the table likesum(txns)
will certainly not trigger the predicate lock, and then it just gets super confusing.As a side note, when you do
That is not safe. But if the
balance
was stored on the accounts table (which we have no reason to believe exists),Would be safe.