Basic Timestamp Protocol
Read Rules
Read aloud is unavailable in this browser.
When Can a Transaction Read?
With the concepts of Read Timestamp (RTS) and Write Timestamp (WTS) established, we can now formalize the Read Rules of the Basic Timestamp Protocol. These rules define precisely when a read operation is allowed, when it must be rejected, and what updates occur on success.
The read rules embody a simple but powerful principle: a transaction should only read values that existed at the time the transaction logically began. Since transaction timestamps define their position in the serialization order, a transaction with timestamp TS(T) should see the database state as it was at logical time TS(T)—meaning it should only read values written by transactions with smaller timestamps.
By the end of this page, you will understand the formal read rules of the Basic Timestamp Protocol, be able to determine whether any read operation should succeed or fail, and know exactly how timestamps are updated upon successful reads. You'll see these rules in action through detailed examples and edge cases.
The Fundamental Read Principle
Before formalizing the rules, let's understand the underlying principle. In timestamp ordering, we want the execution to be equivalent to running all transactions serially in timestamp order. For reads, this means:
A transaction T with timestamp TS(T) should read the value of X that was most recently written by a transaction with timestamp < TS(T).
But here's the challenge: we're not actually running transactions serially. They're interleaved. So when T tries to read X, the current value might have been written by a transaction T' where TS(T') > TS(T). In this case, T would be reading a "future" value—one that shouldn't exist yet from T's perspective.
If T reads a value written by T' where TS(T') > TS(T), we have a temporal violation. In any equivalent serial schedule, T would execute before T', so T should see the value that existed before T' wrote it—not T's value. This is why we must check WTS(X) before allowing reads.
The Core Insight:
The Write Timestamp WTS(X) tells us the timestamp of the transaction that wrote the current value of X. If TS(T) < WTS(X), then a younger transaction wrote the current value, and T cannot read it without violating serializability.
Conversely, if TS(T) ≥ WTS(X), the current value was written by an older or equal-aged transaction, which is consistent with T's position in the serialization order.
| Condition | Meaning | Decision |
|---|---|---|
| TS(T) < WTS(X) | Current value written by younger transaction | REJECT read, abort T |
| TS(T) ≥ WTS(X) | Current value written by older/equal transaction | ALLOW read |
Formal Read Rules
We now present the complete, formal read rules for the Basic Timestamp Protocol. When transaction T requests to read data item X:
Algorithm: Read(X) by Transaction T
1. IF TS(T) < WTS(X) THEN
// T is trying to read a value written in T's "future"
Abort T and restart with new, larger timestamp
RETURN FAILURE
END IF
2. // Read is allowed
Let v = current value of X
3. IF TS(T) > RTS(X) THEN
RTS(X) := TS(T) // Update read timestamp
END IF
4. RETURN v (success)
- •Step 1: WTS Check — The critical serializability check. Compares T's timestamp to the writer's timestamp.
- •Step 2: Value Retrieval — If the check passes, retrieve the current value of X.
- •Step 3: RTS Update — Update the read timestamp if T is younger than all previous readers.
- •Step 4: Return — Return the value to the transaction for use in subsequent operations.
The RTS update in step 3 isn't required for the current read to succeed—it's for future writes. By recording that T has read X, we ensure that any future transaction T' where TS(T') < TS(T) cannot overwrite X and invalidate T's read. RTS is the mechanism that protects readers from belated writers.
Atomicity Requirement:
Steps 1-3 should be atomic (or at least serialized for each data item). Consider what could go wrong without atomicity:
- T checks WTS(X) = 10, TS(T) = 20, check passes
- Before T completes the read, T' writes X, setting WTS(X) = 30
- T reads the new value written by T' (TS(T') = 30 > TS(T) = 20)
T has now read a "future" value despite the initial check passing. Proper implementation requires either:
- Short-lived locks on the timestamp metadata during read processing
- Atomic compare-and-read-and-update operations
- Careful use of memory barriers and atomic primitives
Read Rule Examples
Let's trace the read rules through several scenarios to build intuition.
Example 1: Simple Successful Read
| State/Step | Values | Action/Result |
|---|---|---|
| Initial | X = 100, WTS(X) = 15, RTS(X) = 15 | — |
| T(TS=25) requests Read(X) | Check: 25 ≥ 15? | Yes, proceed |
| Retrieve value | v = 100 | — |
| Update RTS | 25 > 15? | Yes, RTS(X) := 25 |
| Final | X = 100, WTS(X) = 15, RTS(X) = 25 | Return 100 |
T successfully reads X because its timestamp (25) is greater than the writer's timestamp (15). The RTS is updated to 25, protecting the value from writes by transactions with timestamps between 15 and 25.
Example 2: Read Rejection
| State/Step | Values | Action/Result |
|---|---|---|
| Initial | X = 200, WTS(X) = 40, RTS(X) = 40 | — |
| T(TS=20) requests Read(X) | Check: 20 ≥ 40? | No, ABORT |
| Abort T | — | T rolled back, restarts as T' |
| T'(TS=50) requests Read(X) | Check: 50 ≥ 40? | Yes, proceed |
| Complete read | RTS(X) := 50 | Return 200 |
T cannot read X because the current value was written by a transaction with timestamp 40, which is greater than T's timestamp 20. After restarting with a larger timestamp (50), the read succeeds.
Example 3: Multiple Concurrent Reads
| Step | Transaction | WTS(X) | RTS(X) Before | RTS(X) After | Result |
|---|---|---|---|---|---|
| 1 | T50 reads X | 10 | 10 | 50 | Success |
| 2 | T30 reads X | 10 | 50 | 50 | Success (no RTS update) |
| 3 | T60 reads X | 10 | 50 | 60 | Success (RTS updated) |
| 4 | T5 reads X | 10 | 60 | 60 | ABORT (5 < WTS) |
Notice that steps 1-3 all succeed despite being out of timestamp order. As long as each reader's timestamp is ≥ WTS, the read is allowed. The RTS only updates when a younger transaction reads. This is one advantage of timestamp ordering: multiple readers never block each other.
Edge Cases and Special Scenarios
Real systems must handle various edge cases. Let's examine some important scenarios.
Edge Case 1: Reading After Your Own Write
What happens when a transaction reads data it has previously written?
| Step | Operation | State After | Notes |
|---|---|---|---|
| 1 | T(TS=30) writes X = 50 | WTS(X) = 30, RTS(X) = 30 | T writes X |
| 2 | T(TS=30) reads X | Check: 30 ≥ 30? Yes | Same transaction reads |
| 3 | Read completes | RTS(X) = 30 (no change) | 30 > 30 is false, no update |
A transaction can always read its own writes because TS(T) = WTS(X) after T writes X. This is consistent with the intuition that transactions should see their own changes.
Edge Case 2: Reading Newly Inserted Data
When a transaction inserts a new row:
- WTS(X) = TS(T) where T is the inserting transaction
- RTS(X) = TS(T) (initialized to writer's timestamp)
Only transactions with TS ≥ TS(T) can read the new row. This makes sense: the row didn't exist before T, so transactions "older" than T shouldn't see it.
Edge Case 2 touches on the phantom problem. If T1 scans a range and then T2 inserts a new row in that range (where TS(T2) > TS(T1)), T1 won't see it. But if T1 re-scans the range, it still won't see T2's row (blocked by WTS check). This prevents some phantoms, but not all scenarios—range locks or predicates are still needed for complete phantom prevention.
Edge Case 3: Cascading Read Failures
If a transaction reads multiple data items, each read is checked independently. If any read fails, the entire transaction aborts:
- T(TS=15) reads X — WTS(X) = 10 — Success
- T(TS=15) reads Y — WTS(Y) = 10 — Success
- T(TS=15) reads Z — WTS(Z) = 20 — FAIL
- Abort T, including the effects of reading X and Y
In basic timestamp ordering, the "work" done before the failed read (reads don't modify data, but might have influenced later writes) must be carefully handled during abort.
Edge Case 4: Equal Timestamps
What if TS(T) = WTS(X) exactly?
The condition is TS(T) ≥ WTS(X), so TS(T) = WTS(X) is allowed. This typically happens when:
- T is reading its own write (discussed above)
- Two transactions get identical timestamps (should be prevented by timestamp allocation)
For the second scenario, implementation must ensure unique timestamps through:
- Sub-millisecond clock resolution
- Logical counters appended to physical time
- Tie-breaking using transaction IDs
Read-Only Transaction Optimization
Read-only transactions present an interesting optimization opportunity. Since they never write, they can never cause write-after-read conflicts. This observation leads to potential optimizations.
The Problem with Naive Read-Only Handling:
In basic timestamp ordering, every read updates RTS(X) (if the reader is younger than all previous readers). For read-only analytical queries scanning millions of rows, this creates massive write amplification—turning read-only workloads into write-heavy workloads.
- •Skip RTS Updates — Read-only transactions don't need to update RTS because they'll never write. Their timestamp only matters for the WTS check.
- •Snapshot Reads — Assign a read timestamp and use it consistently across all reads, without affecting RTS.
- •Separate Read Queues — Track read-only transactions separately, avoid RTS overhead entirely.
- •Declaration-Based — Allow transactions to declare themselves read-only upfront for optimized handling.
Correctness of Skipping RTS Updates:
Why is it safe for read-only transactions to skip RTS updates? Consider:
- RTS(X) is used to check: "Has a younger transaction read X?"
- This check protects against write-after-read conflicts
- If T is read-only, it will never write
- Therefore, no write-after-read conflict can occur involving T
- RTS(X) doesn't need to record T's read
Caveat: This optimization requires reliable identification of read-only transactions. If a transaction initially appears read-only but later writes, the optimization fails. Systems may require explicit READ ONLY declarations or use speculative execution with fallback.
Read-only optimization is one reason MVCC has largely replaced pure timestamp ordering. In MVCC, reads never update any timestamp—they simply read the appropriate version based on their snapshot. This completely eliminates write amplification for reads, making MVCC far more practical for mixed workloads.
Implementation Considerations
Implementing the read rules correctly requires careful attention to several practical concerns.
| Challenge | Description | Solution Approach |
|---|---|---|
| Atomicity | Check + read + update must be atomic | Short-lived latches or atomic operations |
| Memory Overhead | Every data item needs WTS + RTS | 8-16 bytes per tuple |
| Cache Effects | RTS updates cause cache line invalidation | Batching, lazy updates, or epoch-based approaches |
| Index Entries | Index entries may need separate timestamps | Depends on indexing strategy |
| Recovery | Timestamps must be logged and recoverable | Include in WAL records |
Interaction with Buffer Pool:
When a transaction reads a data item:
- The page containing X must be in the buffer pool
- If not, a disk read is required (I/O)
- The timestamps (WTS, RTS) are part of the tuple or page metadata
- Timestamps may be updated even for "read-only" page accesses
- This can cause pages to become dirty (requiring write-back)
Logging Considerations:
For durability and recovery:
- WTS changes are logged with write operations (already part of normal logging)
- RTS changes are more problematic—should read-only transactions generate log records?
- Some systems skip RTS logging and accept approximate RTS after recovery
- Others log RTS updates, increasing log volume but ensuring exact recovery
Pure timestamp ordering with full RTS tracking is rarely implemented in modern production databases due to these overheads. Instead, systems typically use MVCC (which avoids most RTS overhead) or hybrid approaches that combine timestamp-based reasoning with selective locking.
Read Rules in Protocol Context
The read rules we've formalized are one half of the Basic Timestamp Protocol. Let's understand how they fit into the complete picture.
- •Check: TS(T) ≥ WTS(X)?
- •If no: Abort T
- •If yes: Return value, update RTS
- •Uses WTS to detect conflicts
- •Updates RTS to protect future writes
- •Check: TS(T) ≥ RTS(X)?
- •Check: TS(T) ≥ WTS(X)?
- •If either fails: Abort T
- •If both pass: Write, update WTS
- •Uses RTS + WTS for conflict detection
The Asymmetry:
Notice that reads only check WTS (one condition) while writes check both RTS and WTS (two conditions). This asymmetry reflects the different conflict scenarios:
- Reads conflict with: Younger writes (read-after-write)
- Writes conflict with: Younger reads (write-after-read) AND younger writes (write-after-write)
This is why writes are more likely to be rejected than reads—there are more ways for a write to conflict.
The No-Wait Property:
A key feature of the read rules is that they never wait. The decision is immediate:
- If the check passes, proceed immediately
- If the check fails, abort immediately
There's no queue of waiting transactions as in lock-based protocols. This eliminates deadlocks entirely (transactions never wait for each other) but introduces the cost of aborts and restarts.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.