What is a transaction, and what does it guarantee?
A transaction is a group of statements the database treats as one unit of work. Either all of them take effect or none of them do, and while the transaction is open nobody else sees a half-finished version of the data. That is the whole idea: the unit of atomicity is the transaction, not the statement.
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
owner VARCHAR(120) NOT NULL,
balance_cents INTEGER NOT NULL
);
INSERT INTO accounts (id, owner, balance_cents) VALUES
(1, 'Ada Lovelace', 5000),
(2, 'Grace Hopper', 2000);
BEGIN;
UPDATE accounts SET balance_cents = balance_cents - 1500 WHERE id = 1;
UPDATE accounts SET balance_cents = balance_cents + 1500 WHERE id = 2;
SELECT id, owner, balance_cents FROM accounts ORDER BY id;
COMMIT;
SELECT id, owner, balance_cents FROM accounts ORDER BY id;Both SELECTs return the same two rows: Ada with 3500 cents and Grace with 3500 cents. The SELECT inside the transaction is what shows the intermediate state — the transfer is complete but not yet durable — and the SELECT after the COMMIT shows the same thing, now committed.
Four properties are usually associated with a transaction, and they are worth stating separately. Atomicity means the statements succeed or fail together. Consistency means the data ends up satisfying its constraints. Isolation means concurrent transactions do not see each other's intermediate work. Durability means a committed transaction survives a crash.
The isolation level is the part that is usually under-specified. Read Committed is the default in most engines: a transaction sees rows that were committed before it started, and it can be blocked by a writer holding a lock on the same row. Repeatable Read and Serializable give stronger guarantees — a stable snapshot, or full serialisability — at the cost of more retries and more deadlocks. In PostgreSQL, Repeatable Read can fail with a serialization error that the application has to retry, and that is the correct design rather than a bug.
The practical rule is that a transaction should be as short as possible. Long transactions hold locks, block other writers, and in PostgreSQL prevent vacuum from cleaning up rows that nobody can see any more. Never make a transaction span a network call to a third-party service, because the lock is held while you wait for a response that may never come.