Loading content...
So far, we've treated transactions as monolithic units: either all their changes commit, or all their changes are rolled back. But real-world transactions are often more complex. Applications use savepoints to create rollback points within a transaction, allowing partial undo while keeping earlier work intact.
Consider an e-commerce checkout:
This introduces nested rollback—the ability to undo part of a transaction while preserving other parts. How does ARIES handle this complexity during recovery?
By the end of this page, you will understand how savepoints create rollback boundaries, how CLRs handle partial rollbacks during normal operation, how the undo phase processes transactions with savepoint history, and the interaction between nested rollback and recovery.
A savepoint is a named marker within a transaction that allows partial rollback. When you create a savepoint, the system records the current position in the transaction's log chain. Later, you can rollback to that savepoint, undoing all changes made after it while preserving changes before it.
Savepoint Operations:
What Savepoints Do NOT Do:
12345678910111213141516171819202122232425262728293031323334353637
-- Example: E-commerce checkout with savepoints BEGIN TRANSACTION; -- Phase 1: Reserve inventoryUPDATE inventory SET reserved = reserved + 1 WHERE product_id = 101 AND available > 0; UPDATE inventory SET reserved = reserved + 1 WHERE product_id = 102 AND available > 0; -- Create savepoint after successful reservationSAVEPOINT after_inventory; -- Phase 2: Process payment (might fail)INSERT INTO payment_attempts (order_id, amount, status)VALUES (5001, 299.99, 'pending'); -- Simulate payment processing-- If payment gateway returns failure... -- Rollback just the payment, keep inventory reservedROLLBACK TO SAVEPOINT after_inventory; -- Retry with different payment methodINSERT INTO payment_attempts (order_id, amount, status)VALUES (5001, 299.99, 'pending'); -- This time payment succeedsUPDATE payment_attempts SET status = 'success' WHERE order_id = 5001; -- Complete the transactionCOMMIT; -- Result: Inventory reserved, payment recorded, order complete-- If we had done full ROLLBACK, we'd lose the inventory reservationLog Structure with Savepoints:
When a savepoint is created, a SAVEPOINT log record is written. When rolling back to a savepoint, CLRs are written for each undone operation, and the rollback stops at the savepoint marker.
| LSN | Record Type | Description |
|---|---|---|
| 100 | UPDATE | Reserve product 101 |
| 200 | UPDATE | Reserve product 102 |
| 300 | SAVEPOINT | 'after_inventory' (saveLSN=200) |
| 400 | INSERT | Payment attempt |
| 500 | CLR | Undoing LSN 400 (rollback to savepoint) |
| 600 | INSERT | Second payment attempt |
| 700 | UPDATE | Mark payment success |
| 800 | COMMIT | Transaction complete |
The SAVEPOINT log record typically stores: the savepoint name, the current lastLSN for the transaction (the 'saveLSN'), and possibly lock information needed to restore lock state if rolling back. The saveLSN is crucial—it tells the rollback where to stop.
When an application issues ROLLBACK TO SAVEPOINT, the database performs a partial undo that's conceptually similar to the recovery undo phase—but it operates on a running transaction rather than during crash recovery.
The Process:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
PROCEDURE RollbackToSavepoint(txn, savepointName): // Step 1: Find the savepoint record savepoint = txn.FindSavepoint(savepointName) IF savepoint == NULL: RAISE "Savepoint not found: " + savepointName saveLSN = savepoint.saveLSN // LSN to stop at // Step 2: Start from transaction's current position currentLSN = txn.lastLSN Log.Debug("Rolling back from LSN {currentLSN} to savepoint at {saveLSN}") // Step 3: Walk backward, undoing updates WHILE currentLSN > saveLSN: logRecord = Log.Read(currentLSN) IF logRecord.type == UPDATE: // Undo this update page = BufferPool.Fetch(logRecord.pageId) page.ApplyBeforeImage(logRecord.beforeImage) // Write CLR with undoNextLSN pointing past this record clr = WriteCLR( transactionId: txn.id, pageId: logRecord.pageId, redoInfo: logRecord.beforeImage, undoNextLSN: logRecord.prevLSN, undoLSN: currentLSN ) page.SetPageLSN(clr.lsn) txn.lastLSN = clr.lsn // Move to previous record currentLSN = logRecord.prevLSN ELSE IF logRecord.type == CLR: // A CLR from a previous partial rollback currentLSN = logRecord.undoNextLSN ELSE IF logRecord.type == SAVEPOINT: // We've reached or passed a savepoint record // If this is our target savepoint, stop; otherwise continue IF logRecord.lsn <= saveLSN: BREAK currentLSN = logRecord.prevLSN ELSE: // Other record types (like inner savepoints) currentLSN = logRecord.prevLSN // Step 4: Update transaction state txn.undoNextLSN = saveLSN // For recovery purposes // Step 5: Optionally release locks acquired after savepoint IF config.RELEASE_LOCKS_ON_PARTIAL_ROLLBACK: txn.ReleaseLocksSince(saveLSN) Log.Info("Rollback to savepoint '{savepointName}' complete")Key Observations:
CLRs are written just like full abort: The partial undo creates CLR records that document each undone operation.
The transaction continues: Unlike a full abort, the transaction remains active and can make more updates after the rollback to savepoint.
Chain navigation is the same: We follow PrevLSN pointers, skip CLRs using undoNextLSN, just like recovery undo.
Stop condition differs: Instead of reaching BEGIN, we stop when passing the saveLSN.
Strict 2PL normally holds all locks until commit. Rollback to savepoint can optionally release locks acquired after the savepoint, allowing other transactions to access those resources. However, this is implementation-specific—some systems keep the locks to avoid cascading issues.
Savepoints can be nested—you can create multiple savepoints at different points within a transaction. When rolling back, you can choose which savepoint to target, rolling back to any previously established point.
Example: Multiple Savepoints
Nested Rollback Behavior:
| Rollback Target | What Gets Undone | Transaction State After |
|---|---|---|
| ROLLBACK TO C | U6 only | At savepoint C position |
| ROLLBACK TO B | U4, U5, U6 | At savepoint B position |
| ROLLBACK TO A | U2 through U6 | At savepoint A position |
| ROLLBACK (full) | Everything | Transaction aborted |
Nested Savepoint Considerations:
Rolling back destroys inner savepoints: If you ROLLBACK TO A, savepoints B and C are invalidated—the work they marked is now gone.
Savepoint names can be reused: After rolling back, you can create a new savepoint with the same name at the current position.
Stack-like behavior: Savepoints are often managed as a stack. RELEASE SAVEPOINT pops the top savepoint; ROLLBACK TO pops back to a named point.
123456789101112131415161718192021222324252627282930313233
-- Example: Nested savepoints for complex operation BEGIN TRANSACTION; -- Phase 1: SetupINSERT INTO orders (id, customer) VALUES (1001, 'Alice');SAVEPOINT phase1_complete; -- Phase 2: Add items (with sub-savepoints)INSERT INTO order_items (order_id, product) VALUES (1001, 'Widget');SAVEPOINT item1_added; INSERT INTO order_items (order_id, product) VALUES (1001, 'Gadget');SAVEPOINT item2_added; INSERT INTO order_items (order_id, product) VALUES (1001, 'Gizmo');-- Oops, Gizmo is out of stock! ROLLBACK TO SAVEPOINT item2_added; -- Remove just Gizmo -- Continue with shippingUPDATE orders SET status = 'shipping' WHERE id = 1001;SAVEPOINT ready_to_ship; -- Payment attempt fails-- ROLLBACK TO SAVEPOINT phase1_complete would remove all items!-- Instead, just cancel shipping:ROLLBACK TO SAVEPOINT item2_added; -- Fix and retryUPDATE orders SET status = 'payment_pending' WHERE id = 1001; COMMIT; -- Order created with 2 items, status is payment_pendingAfter ROLLBACK TO a savepoint, any savepoints created after it no longer exist. The log records for those savepoints are still in the log, but they're logically invalidated. Attempting to reference a destroyed savepoint will fail.
When a crash occurs, the undo phase must handle transactions that have savepoint history—including partial rollbacks that created CLRs. The good news: ARIES already handles this correctly through its standard CLR processing.
Scenario: Crash After Partial Rollback
Consider this sequence:
| LSN | Type | PrevLSN | UndoNextLSN | Description |
|---|---|---|---|---|
| 100 | UPDATE | null | T1's first update | |
| 200 | UPDATE | 100 | T1's second update | |
| 250 | SAVEPOINT | 200 | Savepoint 'X' (saveLSN=200) | |
| 300 | UPDATE | 250 | T1's third update | |
| 400 | UPDATE | 300 | T1's fourth update | |
| 500 | CLR | 400 | 300 | Undid LSN 400 |
| 600 | CLR | 500 | 200 | Undid LSN 300 (hit savepoint) |
| 700 | UPDATE | 600 | T1's new update after savepoint rollback | |
| — | CRASH | — | — | System crash here |
Recovery Processing:
Analysis Phase: T1 is in ACTIVE state, lastLSN = 700, undoNextLSN = 700
Redo Phase: Replays all records including CLRs 500 and 600. After redo:
Undo Phase:
The Key Insight: CLRs from the previous partial rollback automatically guide the undo phase to skip LSNs 300 and 400. The CLR chain "remembers" that those updates were already undone!
1234567891011121314151617181920212223242526272829303132333435363738394041
// Trace of undo phase processing for the scenario above UNDO PHASE:ToUndo initialized: {700 → T1} Step 1: Extract max: (700, T1) Read LSN 700: UPDATE, prevLSN=600 → Undo update 700 → Write CLR@800 (undoNextLSN=600) → Update ToUndo: {600 → T1} Step 2: Extract max: (600, T1) Read LSN 600: CLR, undoNextLSN=200 → It's a CLR, don't undo it! → Follow undoNextLSN to 200 → Update ToUndo: {200 → T1} // Notice: We skipped LSNs 500, 400, 300, 250 entirely! // The CLR chain from the partial rollback guides us. Step 3: Extract max: (200, T1) Read LSN 200: UPDATE, prevLSN=100 → Undo update 200 → Write CLR@801 (undoNextLSN=100) → Update ToUndo: {100 → T1} Step 4: Extract max: (100, T1) Read LSN 100: UPDATE, prevLSN=null → Undo update 100 → Write CLR@802 (undoNextLSN=null) → prevLSN is NULL, T1 fully undone → Write END@803 → Remove T1 from ToUndo ToUndo is empty → Undo complete! Final state: All of T1's effects have been removed.ARIES doesn't need special logic for savepoints during recovery! The CLR chain from partial rollbacks naturally encodes what was already undone. Recovery simply follows CLR undoNextLSN pointers like any other CLR, and everything works correctly.
Some database systems support subtransactions or nested transactions, which provide stronger semantics than savepoints. While savepoints are just rollback markers, subtransactions can have their own commit/abort status.
Savepoints vs Subtransactions:
| Feature | Savepoints | Subtransactions |
|---|---|---|
| Rollback granularity | To any named point | Individual subtransaction |
| Commit semantics | Only parent commits | Subtransaction can "commit" (results visible to parent) |
| Abort semantics | Rolls back work | Subtransaction aborts, parent continues |
| Duration | Until rollback or end | Until subtransaction commits/aborts |
| Lock handling | Implementation-specific | Subtransaction may hold/release locks |
How Subtransactions Affect Recovery:
Subtransactions introduce independent units within a parent transaction. Each subtransaction has its own:
12345678910111213141516171819202122232425262728
-- Conceptual example (not standard SQL)-- Some systems support this via nested transaction syntax BEGIN TRANSACTION T1; INSERT INTO log VALUES ('T1 started'); BEGIN SUBTRANSACTION S1; INSERT INTO audit VALUES ('S1 attempt'); -- S1 work here IF [condition]: COMMIT SUBTRANSACTION S1; -- S1 results survive if T1 commits ELSE: ABORT SUBTRANSACTION S1; -- S1 results discarded, T1 continues END SUBTRANSACTION; -- T1 continues regardless of S1 outcome INSERT INTO log VALUES ('After S1'); BEGIN SUBTRANSACTION S2; -- More work COMMIT SUBTRANSACTION S2; END SUBTRANSACTION; COMMIT TRANSACTION T1; -- All committed subtransactions become durable -- If T1 aborts instead, all subtransaction work is lost-- even if the subtransactions individually "committed"Recovery Implications:
Subtransaction commit is provisional: A subtransaction's commit is only durable if the parent transaction commits. If the parent aborts, all subtransaction work is undone.
Undo must track hierarchy: The undo phase needs to understand parent-child relationships:
Log structure becomes more complex: Each subtransaction has its own PrevLSN chain, nested within the parent's chain.
ARIES Support:
Standard ARIES handles savepoints naturally but needs extensions for full nested transactions. Extensions include:
Most production databases (PostgreSQL, Oracle, SQL Server) support savepoints extensively but have limited or no nested transaction support. Full nested transactions are more common in research systems and specialized domains like long-lived transactions.
Let's examine some complex scenarios that combine savepoints, partial rollbacks, and crash recovery to solidify our understanding.
Scenario 1: Multiple Partial Rollbacks Before Crash
12345678910111213141516171819202122232425262728293031
TIMELINE: LSN 100: BEGIN T1LSN 200: UPDATE P1 (A→B)LSN 300: SAVEPOINT X (saveLSN=200)LSN 400: UPDATE P2 (M→N)LSN 500: SAVEPOINT Y (saveLSN=400)LSN 600: UPDATE P3 (X→Y)LSN 700: ROLLBACK TO Y → CLR@700 (undo 600, undoNextLSN=400)LSN 800: UPDATE P4 (J→K)LSN 900: ROLLBACK TO X → CLR@900 (undo 800, undoNextLSN=700) CLR@901 is NOT written because we hit 700 which points to 400 Actually, we need to undo 700's work...wait, CLR@700 isn't undone! CLR@900 (undo 800, undoNextLSN=700) Follow 700 (CLR) → undoNextLSN=400 CLR@901 (undo 400, undoNextLSN=300) Hit savepoint X at 300, stop.LSN 1000: UPDATE P5 (Q→R)--- CRASH --- RECOVERY:- Analysis sees T1 active, lastLSN=1000- Redo replays all records- Undo starts with ToUndo = {1000} Trace: Undo 1000 → CLR@1100 (undoNextLSN=901) Read 901 (CLR) → undoNextLSN=300 Read 300 (SAVEPOINT) → prevLSN=200 Undo 200 → CLR@1101 (undoNextLSN=100) Read 100 (BEGIN) → T1 complete, write ENDWait, the above scenario has an error in reasoning. Let me correct it:
Corrected Scenario 1 Analysis:
When rolling back to savepoint X (at LSN 300, saveLSN=200), we undo:
The CLR chain correctly encodes this. During recovery, following the CLR pointers skips precisely the right records.
| LSN | Type | What It Represents | Part of Active Transaction? |
|---|---|---|---|
| 100 | BEGIN | T1 started | ✓ |
| 200 | UPDATE | P1: A→B | ✓ (not rolled back) |
| 300 | SAVEPOINT X | Marker | ✓ |
| 400 | UPDATE | P2: M→N | ✗ (rolled back to X) |
| 500 | SAVEPOINT Y | Marker (invalidated) | ✗ |
| 600 | UPDATE | P3: X→Y | ✗ (rolled back to Y, then X) |
| 700 | CLR | Undo of 600 | ✓ |
| 800 | UPDATE | P4: J→K | ✗ (rolled back to X) |
| 900-901 | CLRs | Undo of 800, 400 | ✓ |
| 1000 | UPDATE | P5: Q→R | ✓ (not rolled back before crash) |
When analyzing complex scenarios, trace the PrevLSN and undoNextLSN chains carefully. Draw the chains on paper if needed. The key insight is that CLR undoNextLSN pointers create 'shortcuts' that skip exactly the operations that were already undone by partial rollbacks.
Implementing nested rollback support requires careful design decisions. Here are key considerations:
Savepoint Storage:
Databases need to track active savepoints for each transaction:
class Transaction {
id: TransactionId
status: ACTIVE | COMMITTING | ABORTING
lastLSN: LogSequenceNumber
savepoints: Map<SavepointName, SavepointInfo>
}
class SavepointInfo {
name: string
saveLSN: LogSequenceNumber // Log position at creation
lockState?: LockSnapshot // Optional: for lock restoration
cursorsState?: CursorInfo // Optional: for cursor restoration
}
Performance Implications:
Savepoint overhead is minimal: Creating a savepoint is just logging a record and updating in-memory state.
Rollback cost depends on work to undo: More updates after the savepoint = more CLRs = more time.
Recovery doesn't pay extra: The undo phase processes CLRs the same way whether they came from partial rollbacks or full recovery.
SQL Standard Compliance:
SQL:1999 defined savepoint semantics. Key requirements:
Most databases implement these, though details (like lock behavior) vary.
Nested rollback is notoriously tricky to test. Create test cases for: multiple savepoints with interleaved rollbacks, crashes at various points during partial rollback, very long transactions with many savepoints, and savepoint at exact page boundaries in the log.
Nested rollback adds flexibility to transaction processing, allowing applications to implement sophisticated error handling and retry logic. The ARIES recovery system handles this elegantly through its CLR mechanism. Let's consolidate the key insights:
What's Next:
With nested rollback understood, we're ready to explore the completion of the undo phase. The next page covers how ARIES determines when undo is complete, writes END records, prepares for normal operation, and handles any final cleanup tasks.
You now understand nested rollback—how savepoints work, how partial rollbacks create CLR chains, and how recovery automatically handles transactions with savepoint history. The elegance of ARIES is that complex nested rollback scenarios are handled by the same CLR processing that handles simple transaction abort.