Home / Blog / Transactions

sql fundamentals

SQL Transactions Explained: BEGIN, COMMIT, ROLLBACK, and Why They Matter

The mechanism that turns "run these five statements" into "run all five, or none at all."

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 COMMIT returns 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.

LevelPreventsTrade-off
Read CommittedReading uncommitted dataPostgres's default — good balance for most apps
Repeatable ReadNon-repeatable reads within a transactionSQLite's effective default in a transaction
SerializableAll concurrency anomaliesHighest 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.

← Database Schema Design Next: SQLite rowid vs. Primary Key →