A transaction groups one or more SQL statements so they succeed or fail as a single unit. The classic example is a bank transfer: debit one account, credit another. If the debit succeeds but the credit fails halfway through — a crash, a constraint violation, a network drop — you don't want money to have vanished. A transaction makes that impossible by guaranteeing both statements apply, or neither does.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
If anything goes wrong between BEGIN and COMMIT, you run
ROLLBACK instead, and the database behaves as if neither UPDATE
ever happened.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- something's wrong — abort instead of committing
ROLLBACK;
The four guarantees: ACID
Transactions are usually described by four properties, and each one answers a specific failure mode:
- Atomicity — every statement in the transaction applies, or none do. No partial transfers.
- Consistency — a transaction can only move the database from one valid state to another; constraints, foreign keys, and triggers are enforced at commit.
- Isolation — concurrent transactions don't see each other's uncommitted changes. What isolation level guarantees exactly (Read Committed, Repeatable Read, Serializable) varies by database and setting.
- Durability — once
COMMITreturns successfully, the change survives a crash. It's written to disk, not just held in memory.
Without a transaction, statements commit independently
Run the two UPDATEs from above without BEGIN/COMMIT
around them, and each commits the instant it runs. If the second one fails, the first has
already taken effect — there's no automatic way to undo it. This is the failure mode
transactions exist to prevent, and it's easy to hit accidentally when a multi-step change is
written as separate statements instead of one wrapped block.
Isolation levels, briefly
Isolation determines what a transaction can see of other transactions running at the same time. Stricter isolation prevents more anomalies but costs more in locking and reduced concurrency.
| Level | Prevents | Trade-off |
|---|---|---|
| Read Committed | Reading uncommitted data | Postgres's default — good balance for most apps |
| Repeatable Read | Non-repeatable reads within a transaction | SQLite's effective default in a transaction |
| Serializable | All concurrency anomalies | Highest correctness, most contention/retries |
Why "stage, then commit" is a safer editing model
The same idea that protects a bank transfer applies to editing data by hand in a GUI tool. If you're fixing a handful of rows — a typo here, a new record there, a row to delete — applying each change to the database the instant you make it means there's no single point where you can review the whole batch, or cleanly undo it if you spot a mistake three edits in.
Staging every change locally first, and only writing them to the database as one transaction when you explicitly commit, gives you both the review step and the safety net — discard the batch, and it's as if none of it happened, because none of it was ever sent to the database.
This is exactly how QuerySQL's table editor works: every add, edit, duplicate, or delete is staged locally, then applied as a single transaction on Commit — or wiped out entirely with Discard. See the FAQ for more on how it decides what counts as a row's identity when committing.