Loading learning content...
In the vast landscape of SQL commands—hundreds of keywords for querying, modifying, and managing databases—three simple statements have outsized importance: BEGIN, COMMIT, and ROLLBACK. These transaction control statements determine whether your changes become permanent reality or vanish as if they never happened.
These commands may seem trivial, but understanding their exact semantics prevents catastrophic errors. What happens if you COMMIT twice? What if you forget to BEGIN? What does ROLLBACK do to locks? This page answers these questions with precision.
By the end of this page, you will understand the exact semantics of BEGIN, COMMIT, and ROLLBACK, how these commands differ across major database systems, what happens mechanically when each command executes, error handling patterns for transaction control, and advanced variations like COMMIT AND CHAIN.
The BEGIN statement (also START TRANSACTION in some systems) marks the starting point of a transaction. It tells the database: 'From this point forward, treat all operations as a single atomic unit until I say otherwise.'
Formal Semantics:
When BEGIN executes:
12345678910111213141516171819202122232425262728
-- Basic transaction startBEGIN; -- Equivalent formsSTART TRANSACTION; -- With explicit isolation levelBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- With read-only hintBEGIN TRANSACTION READ ONLY; -- Combined optionsSTART TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY; -- Check transaction statusSELECT txid_current(); -- Returns current transaction IDSELECT pg_is_in_recovery(); -- Check if replica -- Transaction state querySELECT pid, state, xact_start, query_start, NOW() - xact_start AS transaction_ageFROM pg_stat_activity WHERE pid = pg_backend_pid();In PostgreSQL, executing BEGIN while already in a transaction generates a warning but continues the existing transaction. In SQL Server, it increments @@TRANCOUNT, creating artificial 'nesting'. Always check transaction state before assuming you're starting fresh.
Transaction Options Explained:
| Option | Effect | Use Case |
|---|---|---|
| ISOLATION LEVEL | Sets how transaction sees other transactions' changes | SERIALIZABLE for financial; READ COMMITTED for general use |
| READ ONLY | Transaction will only read, not modify data | Reporting queries; optimizer hints for better plans |
| READ WRITE | Transaction may read and modify (default) | Most operations |
| WITH CONSISTENT SNAPSHOT (MySQL) | Creates point-in-time view of database | Ensuring consistent reads across multiple queries |
| WITH MARK (SQL Server) | Creates named recovery point in transaction log | Point-in-time recovery to specific business events |
The COMMIT statement makes all changes in the current transaction permanent. Once committed, changes survive any subsequent failure—this is the durability guarantee of ACID. COMMIT is the point of no return.
Formal Semantics:
When COMMIT executes:
12345678910111213141516171819202122
-- Basic commitBEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2;COMMIT; -- Equivalent formsCOMMIT WORK;COMMIT TRANSACTION;END; -- PostgreSQL alias for COMMIT -- Commit and start new transaction immediatelyCOMMIT AND CHAIN;-- Equivalent to: COMMIT; BEGIN;-- New transaction inherits same isolation level -- Prepared transaction (two-phase commit)BEGIN; UPDATE distributed_data SET value = 42 WHERE id = 1;PREPARE TRANSACTION 'tx_for_distributed_commit';-- Transaction is now in prepared state, can survive crashes-- Later: COMMIT PREPARED 'tx_for_distributed_commit';The COMMIT command doesn't actually write data pages to disk—it writes the commit record to the transaction log and ensures the log is on stable storage. Data pages may remain in memory (dirty) and be written later. But because the log contains enough information to redo all changes, durability is guaranteed.
COMMIT Performance Considerations:
COMMIT involves synchronous I/O (fsync) to ensure durability. This has significant performance implications:
The ROLLBACK statement undoes all changes made within the current transaction, restoring the database to the state it was in when the transaction began. It's the 'undo everything' button.
Formal Semantics:
When ROLLBACK executes:
1234567891011121314151617181920212223242526
-- Basic rollbackBEGIN; UPDATE accounts SET balance = balance - 1000000 WHERE id = 1; -- Oops, that's a million dollars!ROLLBACK;-- Phew, nothing happened -- Equivalent formsROLLBACK WORK;ROLLBACK TRANSACTION;ABORT; -- PostgreSQL alias for ROLLBACK -- Rollback and start new transactionROLLBACK AND CHAIN;-- Equivalent to: ROLLBACK; BEGIN; -- Rollback to savepoint (partial rollback)BEGIN; UPDATE orders SET status = 'processing' WHERE id = 1; SAVEPOINT before_risky; DELETE FROM orders WHERE id = 2; -- Oops, wrong row!ROLLBACK TO SAVEPOINT before_risky; -- orders.id=1 change is preserved -- orders.id=2 delete is undone UPDATE orders SET status = 'confirmed' WHERE id = 1;COMMIT;Unlike COMMIT, ROLLBACK cannot fail in the traditional sense. The database always has enough undo information to reverse a transaction's changes. If the system crashes during rollback, recovery will complete the rollback automatically.
What happens when an error occurs in the middle of a transaction? The answer varies significantly between database systems, and getting this wrong leads to subtle bugs.
| Database | After Statement Error | Transaction State | Required Action |
|---|---|---|---|
| PostgreSQL | Transaction marked 'aborted' | All subsequent statements fail | Must ROLLBACK before any new work |
| MySQL | Statement rolled back, transaction continues | Still active and usable | Can continue with other statements or COMMIT |
| SQL Server | Depends on XACT_ABORT setting | Varies (see below) | Check XACT_STATE() before deciding |
| Oracle | Statement rolled back, transaction continues | Still active | Can continue, COMMIT, or ROLLBACK |
1234567891011121314151617181920
-- PostgreSQL: Error aborts entire transactionBEGIN; INSERT INTO users (id, name) VALUES (1, 'Alice'); -- OK INSERT INTO users (id, name) VALUES (1, 'Bob'); -- Error: duplicate key -- Transaction is now in 'aborted' state -- This will fail with: "current transaction is aborted" SELECT * FROM users; ROLLBACK; -- Only option: rollback -- Workaround: Use savepointsBEGIN; SAVEPOINT before_insert; INSERT INTO users (id, name) VALUES (1, 'Bob'); -- Might fail -- If it fails, we can recover: ROLLBACK TO SAVEPOINT before_insert; -- Transaction is still usable INSERT INTO users (id, name) VALUES (2, 'Bob'); -- Try with different IDCOMMIT;PostgreSQL's behavior (aborting the transaction on any error) is stricter than other databases. This prevents accidentally committing partial work but requires more careful error handling. Use savepoints for operations that might fail if you need to continue after errors.
Besides explicit COMMIT and ROLLBACK, databases perform automatic transaction ending in certain situations. Understanding these cases prevents surprises.
123456789101112131415
-- MySQL: DDL causes implicit commitSTART TRANSACTION; INSERT INTO users (name) VALUES ('Alice'); -- Part of transaction CREATE TABLE temp_log (id INT); -- Implicit COMMIT here! -- The INSERT is now committed, regardless of what follows INSERT INTO users (name) VALUES ('Bob');ROLLBACK; -- Only rolls back Bob, not Alice! -- PostgreSQL: DDL is transactionalBEGIN; INSERT INTO users (name) VALUES ('Alice'); CREATE TABLE temp_log (id INT);ROLLBACK; -- Both INSERT and CREATE are undone!PostgreSQL uniquely supports transactional DDL—CREATE, ALTER, DROP can be part of a transaction and rolled back. This enables powerful deployment patterns like: BEGIN; DROP TABLE old; ALTER TABLE new RENAME TO old; COMMIT; If any step fails, the rollback restores the original state.
Beyond basic BEGIN/COMMIT/ROLLBACK, databases offer advanced transaction control features for specialized use cases.
| Feature | Syntax Example | Purpose |
|---|---|---|
| COMMIT AND CHAIN | COMMIT AND CHAIN | Commit and immediately start new transaction with same properties |
| COMMIT WORK RELEASE | COMMIT RELEASE (MySQL) | Commit and disconnect; useful for connection pooling cleanup |
| COMMIT WRITE WAIT | COMMIT WRITE IMMEDIATE WAIT (Oracle) | Control durability vs. performance trade-off |
| PREPARE TRANSACTION | PREPARE TRANSACTION 'id' (PostgreSQL) | First phase of two-phase commit for distributed transactions |
| XA Transactions | XA START, XA PREPARE, XA COMMIT (MySQL) | Cross-database or cross-resource manager transactions |
| DELAYED DURABILITY | BEGIN TRAN WITH DELAYED_DURABILITY=ON (SQL Server) | Trade durability guarantee for performance |
12345678910111213141516171819202122
-- Two-Phase Commit (2PC) for distributed transactions-- Requires max_prepared_transactions > 0 in postgresql.conf -- Phase 1: Prepare on Database ABEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1;PREPARE TRANSACTION 'transfer_2024_001';-- Transaction is now 'prepared' - survives crashes -- Meanwhile, on Database B, similar prepare happens... -- Phase 2: Commit both (or rollback both)-- On Database A:COMMIT PREPARED 'transfer_2024_001';-- On Database B:COMMIT PREPARED 'transfer_2024_001'; -- If any prepare fails, rollback all:ROLLBACK PREPARED 'transfer_2024_001'; -- View prepared transactionsSELECT * FROM pg_prepared_xacts;Two-phase commit (2PC) guarantees atomicity across distributed databases but introduces a blocking window where resources are locked waiting for coordinator decision. If the coordinator crashes between prepare and commit, resources may remain locked until manual intervention. Use with care.
Proper use of BEGIN, COMMIT, and ROLLBACK is essential for reliable applications. Here are battle-tested practices from production systems:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// ✅ GOOD: Guaranteed cleanup with try/finally patternasync function transferWithProperHandling( fromId: string, toId: string, amount: number) { const client = await pool.connect(); try { await client.query('BEGIN'); // Validate first const { rows: [sender] } = await client.query( 'SELECT balance FROM accounts WHERE id = $1 FOR UPDATE', [fromId] ); if (sender.balance < amount) { throw new InsufficientFundsError(sender.balance, amount); } // Perform transfer await client.query( 'UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId] ); await client.query( 'UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId] ); await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); throw error; // Re-throw after cleanup } finally { client.release(); // Always release connection }} // ❌ BAD: No error handling, possible leaked transactionasync function transferDangerous(fromId: string, toId: string, amount: number) { await pool.query('BEGIN'); await pool.query(`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`); // If this next line throws, transaction remains open! await pool.query(`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${toId}`); await pool.query('COMMIT');}If you don't rollback before releasing a connection to a pool, the next user might inherit your uncommitted transaction. Always ensure transactions are explicitly ended before returning connections.
These three commands form the core vocabulary of transaction control. Let's consolidate our understanding:
What's Next:
We've covered explicit transaction control. The next page explores implicit vs explicit transactions in depth—when each mode is appropriate, how to configure and detect the current mode, and the subtle bugs that arise from mode confusion.
You now have a comprehensive understanding of the three fundamental transaction control statements. This knowledge is essential for writing correct, robust database code and debugging transaction-related issues in production systems.