Loading learning content...
There comes a moment in every successful transaction's life when its changes become irrevocable. No crash can undo them. No failure can erase them. No rollback can reverse them. This moment is when a transaction enters the Committed state.
The Committed state is not merely a bookkeeping designation—it represents a fundamental guarantee that the database makes to you as a user or application. It is the fulfillment of the Durability promise in ACID: once committed, your transaction's effects will survive any subsequent failure of the system.
But what does 'committed' actually mean at a technical level? How does the database guarantee permanence? What happens after a transaction commits? These questions are essential for understanding database reliability and designing robust applications.
By the end of this page, you will understand the formal definition of the Committed state, comprehend the guarantees that committed transactions receive, learn what post-commit activities occur, understand the relationship between commit and recovery, and appreciate the implications of commit for application design.
Let's establish a precise understanding of what it means for a transaction to be Committed.
Formal Definition:
A transaction T is in the Committed state if and only if:
Using formal notation:
T ∈ Committed ⟺
(previous_state(T) = PartiallyCommitted) ∧
(commit_record_durable(T) = true) ∧
(durability_guaranteed(T) = true)
The Committed State is Terminal:
Unlike Active or Partially Committed, the Committed state is a terminal state. Once a transaction enters Committed, it never transitions to any other state. The transaction's lifecycle is complete.
| Property | Value | Implication |
|---|---|---|
| State Type | Terminal (final) | No more transitions possible |
| Operations Allowed | None | Transaction has completed all work |
| Data Changes | Permanent | Changes survive any failure |
| Locks | Released | Other transactions can access data |
| Rollback Possible | No | Only compensating transactions can undo effects |
| Visible to Others | Yes | Changes are visible per isolation rules |
Important distinction: 'Commit' is the action (the COMMIT command). 'Committed' is the state. A transaction can attempt to commit but fail (transitioning to Failed instead of Committed). Only when the commit is fully successful does the transaction enter the Committed state.
The Complete Transaction State Diagram:
With the Committed state, we can now show a significant portion of the transaction state machine:
The defining characteristic of the Committed state is the durability guarantee. Let's examine what this means in rigorous terms.
What Durability Guarantees:
When a transaction is in the Committed state, the database guarantees that:
Persistence — The transaction's changes will be retained indefinitely (or until explicitly modified by a future transaction)
Crash Survival — If the system crashes immediately after commit, recovery will restore the committed changes
Media Survival (with proper backup) — The changes can be recovered even from catastrophic storage failure if proper backup/recovery procedures are followed
Correctness — The committed state represents a consistent state of the database (all constraints are satisfied)
What Durability Does NOT Guarantee:
The Technical Basis of Durability:
Durability is achieved through a combination of mechanisms:
1. Write-Ahead Logging (WAL)
All changes are logged before being applied to data files. The commit record in the log is the authoritative marker of a committed transaction.
2. Stable Storage
Logs are written to storage that survives power loss and crashes. This typically means:
3. Recovery Algorithms
After a crash, the recovery process:
12345678910111213141516171819202122232425262728293031
-- Demonstrating durability: data survives restart -- Step 1: Insert data and commitBEGIN;INSERT INTO audit_log (event_type, message, created_at)VALUES ('TEST', 'Testing durability', NOW()); -- Verify data exists in transactionSELECT * FROM audit_log WHERE event_type = 'TEST'; -- Commit - transaction enters Committed stateCOMMIT; -- Step 2: Force a checkpoint to ensure data is written-- (In practice, the log force at commit is sufficient for durability)CHECKPOINT; -- Step 3: Simulate or perform a database restart-- The data will still be there after restart because:-- 1. The commit record was forced to WAL at commit time-- 2. On restart, recovery replays any needed WAL records -- Step 4: After restart, verify data persistsSELECT * FROM audit_log WHERE event_type = 'TEST';-- Result: Data is present, demonstrating durability -- View WAL history to understand the durability mechanismSELECT * FROM pg_stat_wal; -- Check transaction status tracking (pg_xact)-- This is updated atomically with the commitTo truly test durability, perform a 'crash' immediately after commit—kill the database process, cut power, or trigger an immediate shutdown. On restart, verify your committed data is present. This is how database vendors validate their durability claims. Never assume durability; verify it in your specific environment.
Once a transaction enters the Committed state, several post-commit activities occur. These activities clean up resources and finalize the transaction's effects.
1. Lock Release:
When a transaction commits, all locks it held are released. This is critical for system concurrency:
Lock release is typically immediate upon commit, though some systems may batch this for efficiency.
123456789101112131415161718192021
-- Session 1: Acquire lockBEGIN;SELECT * FROM accounts WHERE id = 100 FOR UPDATE;-- Exclusive lock now held on row id=100 -- Session 2 (before Session 1 commits): This blocks!BEGIN;UPDATE accounts SET balance = 1000 WHERE id = 100;-- Waiting... -- Session 1: Commit (entering Committed state)COMMIT;-- Lock released immediately -- Session 2: Now proceeds!-- The UPDATE completes because lock was released -- Verify no locks remain from committed transaction-- (PostgreSQL)SELECT * FROM pg_locks WHERE pid = <session1_pid>;-- Returns: Empty - all locks released2. Transaction Descriptor Cleanup:
The internal data structures that tracked the transaction are deallocated:
3. Log Space Management:
Once a transaction commits (and its effects are checkpointed to data files), its log records become candidates for recycling:
4. MVCC Version Management:
In MVCC systems, commit affects version visibility:
| Resource | During Transaction | After Commit | Eventually |
|---|---|---|---|
| Locks | Held for isolation | Released immediately | N/A |
| Transaction Descriptor | Allocated in memory | Deallocated | N/A |
| Undo Log Records | Needed for rollback | Not needed for this txn | Reclaimed after checkpoint |
| Redo Log Records | Accumulated | May still be needed | Reclaimed after checkpoint |
| MVCC Old Versions | Still visible to some txns | Visibility decreases | Vacuumed when no longer needed |
| Transaction ID | In active list | Removed from active list | May be reused (wrap-around) |
Commit is 'lighter' than rollback in many ways. Commit releases locks and cleans up, but doesn't need to undo any changes. Rollback, by contrast, must actively undo all modifications. This is one reason why successful commit is more efficient than abort (and why you should aim for transactions to commit, not abort, under normal circumstances).
Understanding the Committed state is incomplete without understanding how commit interacts with database recovery. This relationship is at the heart of durability.
The Recovery Scenario:
Consider what happens when a system crashes:
Recovery Must:
The Recovery Algorithm (ARIES-style simplified):
Phase 1: Analysis
Phase 2: Redo
Phase 3: Undo
1234567891011121314151617181920212223242526272829303132333435363738
// Simplified recovery algorithm demonstrating commit status importance function recover_database() { // Phase 1: Analysis - identify transaction states committed_txns = Set() active_txns = Set() for each log_record in log from checkpoint: if log_record.type == BEGIN: active_txns.add(log_record.transaction_id) elif log_record.type == COMMIT: // THIS IS THE KEY: presence of COMMIT record committed_txns.add(log_record.transaction_id) active_txns.remove(log_record.transaction_id) elif log_record.type == ABORT: active_txns.remove(log_record.transaction_id) // After scanning: committed_txns has committed transactions // active_txns has transactions that were in progress (never committed) // Phase 2: Redo - apply all logged changes for each log_record in log from checkpoint: if log_record is a data modification: page = read_page(log_record.page_id) if page.lsn < log_record.lsn: // Page doesn't have this change - apply it apply_redo(log_record, page) // Phase 3: Undo - roll back uncommitted transactions for txn_id in active_txns: // These transactions never reached Committed state // Their changes must be undone rollback_transaction(txn_id) // Database is now consistent: // - All Committed transactions' effects are present // - All Active (uncommitted) transactions' effects are removed}In recovery, the PRESENCE or ABSENCE of the commit record in the durable log is the sole determinant of whether a transaction's effects are preserved. There is no gray area. No commit record = abort. Has commit record = committed. This binary distinction is why the Partially Committed state's log force operation is so critical.
When a transaction commits, its changes become potentially visible to other transactions. However, the exact visibility rules depend on the isolation level configuration.
The Principle of Visibility:
Once a transaction enters the Committed state:
Visibility by Isolation Level:
| Isolation Level | Visibility of Just-Committed Data | Explanation |
|---|---|---|
| READ UNCOMMITTED | Immediately visible | Can see data even before commit (not relevant here) |
| READ COMMITTED | Visible on next statement | Each statement sees latest committed data |
| REPEATABLE READ | Visible only if txn started after commit | Transaction sees snapshot from its start |
| SERIALIZABLE | Depends on implementation | May use locking or snapshot isolation |
12345678910111213141516171819202122232425262728293031323334353637
-- Demonstrating commit visibility at different isolation levels -- === Setup ===CREATE TABLE products (id int PRIMARY KEY, price decimal(10,2));INSERT INTO products VALUES (1, 100.00); -- === READ COMMITTED Example ===-- Session 1:BEGIN ISOLATION LEVEL READ COMMITTED;SELECT price FROM products WHERE id = 1; -- Returns: 100.00 -- Session 2: (concurrent) BEGIN; UPDATE products SET price = 150.00 WHERE id = 1; COMMIT; -- Now committed -- Session 1: Next read sees the committed changeSELECT price FROM products WHERE id = 1; -- Returns: 150.00 (!)COMMIT; -- === REPEATABLE READ Example ===-- Session 1:BEGIN ISOLATION LEVEL REPEATABLE READ;SELECT price FROM products WHERE id = 1; -- Returns: 150.00 -- Session 2: (concurrent) BEGIN; UPDATE products SET price = 200.00 WHERE id = 1; COMMIT; -- Now committed -- Session 1: STILL sees original value (snapshot isolation)SELECT price FROM products WHERE id = 1; -- Returns: 150.00 (unchanged!)COMMIT; -- Only AFTER Session 1 commits and starts new transaction:BEGIN ISOLATION LEVEL REPEATABLE READ;SELECT price FROM products WHERE id = 1; -- NOW Returns: 200.00MVCC and Commit Visibility:
In MVCC-based systems (PostgreSQL, Oracle, MySQL InnoDB), commit visibility is determined by comparing transaction IDs or timestamps:
Each transaction gets a snapshot of 'which transactions are visible'
A committed transaction is visible if:
The commit timestamp (or transaction ID) is recorded when a transaction commits
This timestamp is used by readers to determine visibility
A common misconception is that committed data is immediately visible everywhere. In reality: (1) MVCC isolation levels may hide committed data from transactions that started earlier, (2) In distributed systems, replication delays mean reads from replicas may not see recent commits, (3) Application caches may serve stale data even after database commit. Design your applications with these visibility semantics in mind.
Understanding the Committed state has profound implications for application design. Here are key considerations for building robust applications.
1. Once Committed, You Can't Rollback:
Unlike Active transactions where ROLLBACK reverses changes, committed changes are permanent. To 'undo' a committed transaction, you must create a compensating transaction—a new transaction that logically reverses the effects.
123456789101112131415161718192021
-- Original transaction (now committed)BEGIN;INSERT INTO orders (id, customer_id, amount) VALUES (1001, 42, 500.00);UPDATE accounts SET balance = balance - 500.00 WHERE customer_id = 42;COMMIT; -- This is now permanent -- Oops! The order was an error. Cannot ROLLBACK - already committed.-- Must create a compensating transaction: BEGIN;-- Compensate the balance deductionUPDATE accounts SET balance = balance + 500.00 WHERE customer_id = 42; -- Mark the order as cancelled (or delete, depending on requirements)UPDATE orders SET status = 'CANCELLED' WHERE id = 1001; -- Record the compensation for auditINSERT INTO order_audit (order_id, action, reason, timestamp)VALUES (1001, 'CANCEL', 'Order entered in error', NOW()); COMMIT; -- Compensation is also committed2. Commit Confirmation Matters:
Until you receive confirmation that a commit succeeded, you cannot assume your data is durable. This is especially important for:
3. Designing for Durability Failures:
In rare cases, even 'committed' transactions might be lost if durability infrastructure fails (e.g., both disks in a mirror fail simultaneously). Critical applications should:
There's an irony in database commit: we work hard to make data permanent, then often need to change it later. Good database design anticipates change: use soft deletes, maintain history tables, design flexible schemas. The permanence of commit should be viewed as 'permanent until intentionally modified,' not 'permanent forever.'
How can you confirm that a transaction has committed? How do you view commit history? Different database systems provide various tools for observing committed transactions.
PostgreSQL: Transaction Status and History:
123456789101112131415161718192021
-- Check if a specific transaction ID committed-- (Requires track_commit_timestamp = on)SELECT pg_xact_commit_timestamp('12345'::xid) AS commit_time;-- Returns timestamp if committed, NULL if not -- View recent prepared transactions (2PC)SELECT * FROM pg_prepared_xacts; -- See commit status in pg_xact (internal, not directly queryable)-- But you can check via: SELECT txid_status('12345'); -- 'committed', 'aborted', 'in progress', or NULL -- View current snapshot informationSELECT txid_current_snapshot();-- Returns: xmin:xmax:xip_list-- Transactions below xmin are committed and visible -- Check if specific data change has committed (indirect)SELECT xmin, xmax, * FROM my_table WHERE id = 100;-- xmin shows the transaction that created/modified the row-- If xmin < current_xmin in snapshot, it's committed and visibleAudit Logging for Commit Tracking:
For applications that need to track commit history beyond what the database provides, implement audit logging:
123456789101112131415161718192021222324252627282930313233343536373839
-- Create audit log tableCREATE TABLE transaction_audit ( audit_id BIGSERIAL PRIMARY KEY, transaction_id VARCHAR(100) NOT NULL, operation_type VARCHAR(50) NOT NULL, table_affected VARCHAR(100) NOT NULL, record_id BIGINT, old_values JSONB, new_values JSONB, committed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), committed_by VARCHAR(100) DEFAULT CURRENT_USER); -- Use a trigger to log changesCREATE OR REPLACE FUNCTION log_changes()RETURNS TRIGGER AS $$BEGIN INSERT INTO transaction_audit (transaction_id, operation_type, table_affected, record_id, old_values, new_values) VALUES ( txid_current()::text, TG_OP, TG_TABLE_NAME, COALESCE(NEW.id, OLD.id), CASE WHEN TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN row_to_json(OLD) ELSE NULL END, CASE WHEN TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN row_to_json(NEW) ELSE NULL END ); RETURN COALESCE(NEW, OLD);END;$$ LANGUAGE plpgsql; -- Apply trigger to tables you want to auditCREATE TRIGGER audit_orders AFTER INSERT OR UPDATE OR DELETE ON orders FOR EACH ROW EXECUTE FUNCTION log_changes(); -- Now all committed changes to 'orders' are loggedWe've thoroughly explored the Committed state—the successful terminal state that represents the achievement of durability. Let's consolidate our understanding:
What's Next:
We've now covered the 'happy path' of transaction states: Active → Partially Committed → Committed. But what happens when things go wrong? The next page explores the Failed state—the state a transaction enters when an error or failure prevents successful completion.
You now have a comprehensive understanding of the Committed state—the successful endpoint of a transaction's lifecycle. You understand the durability guarantee, how recovery preserves committed transactions, visibility rules, and application design considerations. This completes your understanding of the successful transaction path.