This is actually harder than it looks.
More than one person might concurrently be inserting a row, and their commit orders might not be the same as the order in which the rows are created. That means that a trigger calculating the balance won't see rows it should and the balance will be wrong.
To make this work reliably, you must LOCK TABLE ... IN EXCLUSIVE MODE
before doing an INSERT
, or you must use SERIALIZABLE
isolation in 9.2 or newer. Both introduce the need to re-try failed transactions:
SERIALIZABLE
will abort a problematic transaction and force you to re-try it; and
- Taking a lock in a trigger involves a lock upgrade, which can deadlock, causing the transaction to be aborted.
so your app must be prepared to re-try failed transactions.
To avoid this you can use READ COMMITTED
isolation and have the app explicitly LOCK TABLE ... IN EXCLUSIVE MODE
the table of interest before it tries to do anything else with it, even SELECT
from it, if it thinks it might later need to INSERT
a row into it. However, concurrency will then suck somewhat.
So, given that, you'd write something like:
CREATE OR REPLACE FUNCTION balance_insert_trigger()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
-- This can deadlock unless you already took the lock earlier
-- in the transaction; we put it here only for safety to make
-- sure to block concurrent inserts.
LOCK TABLE mytable IN EXCLUSIVE MODE;
IF tg_op = 'INSERT' THEN
NEW.balance = (SELECT sum(in) - sum(out) FROM mytable);
RETURN NEW
ELSE
-- It's not safe to DELETE FROM or UPDATE this table because of the
-- running balance.
RAISE EXCEPTION '% operation not permitted on this table', tg_op;
END IF;
END;
$$;
CREATE TRIGGER balance_insert_trigger
BEFORE INSERT OR UPDATE OR DELETE ON mytable
FOR EACH ROW EXECUTE PROCEDURE balance_insert_trigger();
then always:
BEGIN;
LOCK TABLE mytable IN EXCLUSIVE MODE;
INSERT INTO mytable ...;
COMMIT;
If you want to support UPDATE
and DELETE
too, then things get exciting.