Loading content...
A transaction's journey through a database system has many phases: it begins, reads data, performs computations, writes results, and commits. At which exact point should the system assign a timestamp? This seemingly simple question has profound implications for correctness, performance, and system behavior.
Assign too early, and the timestamp might not reflect the transaction's actual position in the serialization order. Assign too late, and you might miss opportunities to detect conflicts before wasted work. The timestamp assignment policy is a fundamental design decision that shapes how the entire concurrency control mechanism behaves.
In this page, we'll examine when timestamps are assigned, what factors influence this decision, and how different policies affect system performance and correctness.
By the end of this page, you will understand the different timestamp assignment moments (begin, first operation, commit), the implications of each choice for serializability and abort rates, how read and write timestamps are maintained for data items, the interaction between assignment policy and conflict detection, and practical considerations in production systems.
The timestamp assignment moment defines a transaction's position in the logical serial order. There are three primary choices for when to assign:
1. At Transaction Start (BEGIN)
The most common approach: assign the timestamp immediately when the transaction begins, before any operations execute.
BEGIN TRANSACTION → Timestamp assigned here
READ(A)
WRITE(B, value)
COMMIT
Implications:
2. At First Operation
Defer assignment until the transaction actually accesses data:
BEGIN TRANSACTION → No timestamp yet
READ(A) → Timestamp assigned here (first access)
WRITE(B, value)
COMMIT
Implications:
3. At Commit Time
Wait until the transaction is ready to commit before assigning:
BEGIN TRANSACTION → No timestamp yet
READ(A) → Record reads/writes without timestamp
WRITE(B, value)
COMMIT → Timestamp assigned here
Implications:
| Strategy | When Assigned | Abort Risk | Validation Point | Use Case |
|---|---|---|---|---|
| Begin-time | Transaction start | Higher for long transactions | On each operation | Standard timestamp ordering |
| First-operation | First read/write | Moderate | On each operation | Reduced idle-time conflicts |
| Commit-time | At COMMIT | Detected late (at commit) | Commit validation | Optimistic concurrency control |
The 'classic' timestamp ordering protocol we'll study in detail assumes begin-time assignment. Each transaction gets its timestamp at BEGIN, and all conflict checks compare this fixed timestamp against data item timestamps. This simplicity makes the protocol easier to analyze and implement correctly.
Let's examine the mechanics of timestamp assignment at transaction start, the most common approach in timestamp ordering protocols.
Step-by-Step Process:
Transaction Initiates: Client sends BEGIN TRANSACTION request
Allocate Transaction Context: System creates internal transaction state:
Generate Timestamp: Obtain unique, monotonically increasing value:
Record Assignment: Store timestamp in transaction context:
transaction.timestamp = generated_valueAcknowledge to Client: Return success, transaction is now active
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
from dataclasses import dataclass, fieldfrom typing import Dict, Set, Optionalfrom enum import Enumimport threading class TransactionStatus(Enum): ACTIVE = "active" COMMITTED = "committed" ABORTED = "aborted" @dataclassclass TransactionContext: """ Internal representation of an active transaction. Timestamp is assigned at creation and never changes. """ txn_id: int timestamp: int # Assigned at begin, immutable status: TransactionStatus = TransactionStatus.ACTIVE read_set: Set[str] = field(default_factory=set) write_set: Dict[str, any] = field(default_factory=dict) class TransactionManager: """ Manages transaction lifecycle including timestamp assignment. """ def __init__(self, timestamp_generator): self._ts_generator = timestamp_generator self._txn_counter = 0 self._active_transactions: Dict[int, TransactionContext] = {} self._lock = threading.Lock() def begin_transaction(self) -> TransactionContext: """ Starts a new transaction with timestamp assignment. Returns: New TransactionContext with assigned timestamp """ with self._lock: # Step 1: Generate unique transaction ID self._txn_counter += 1 txn_id = self._txn_counter # Step 2: Assign timestamp (the critical moment!) timestamp = self._ts_generator.get_timestamp() # Step 3: Create transaction context txn = TransactionContext( txn_id=txn_id, timestamp=timestamp ) # Step 4: Register as active self._active_transactions[txn_id] = txn return txn def get_transaction(self, txn_id: int) -> Optional[TransactionContext]: """Retrieves active transaction by ID.""" return self._active_transactions.get(txn_id) def abort_transaction(self, txn: TransactionContext) -> None: """Aborts transaction, removes from active set.""" with self._lock: txn.status = TransactionStatus.ABORTED self._active_transactions.pop(txn.txn_id, None) def commit_transaction(self, txn: TransactionContext) -> None: """Commits transaction (after all validation passes).""" with self._lock: txn.status = TransactionStatus.COMMITTED self._active_transactions.pop(txn.txn_id, None)Some systems use the same value for transaction ID and timestamp—they're both just incrementing counters. Others separate them: the transaction ID identifies the transaction internally (for logging, debugging), while the timestamp determines serialization order. Separating them provides flexibility but adds a small amount of complexity.
While transaction timestamps are assigned once at begin, data item timestamps are updated dynamically as operations execute. These data-level timestamps enable the protocol to detect conflicts.
Two Timestamps Per Data Item:
For each data item Q in the database:
W-timestamp(Q) — Write Timestamp
R-timestamp(Q) — Read Timestamp
Why Both Are Needed:
W-timestamp enables detecting write-write conflicts (later writer must have larger timestamp). R-timestamp enables detecting write-read conflicts (cannot write "before" a read that already happened).
Updating Data Item Timestamps:
On Successful Read of Q by Transaction T:
R-timestamp(Q) = max(R-timestamp(Q), TS(T))
The read timestamp advances to track the "latest" reader.
On Successful Write of Q by Transaction T:
W-timestamp(Q) = TS(T)
R-timestamp(Q) = TS(T) // Also update read timestamp
Both timestamps update—the write establishes both a new "written by" and a new "seen by" point.
On Aborted Transaction: No updates needed—the transaction never "happened" from the database's perspective. However, if R-timestamp was already updated on a read, this doesn't need to be rolled back (conservative).
| Action | TS(T) | W-TS(Q) | R-TS(Q) | Notes |
|---|---|---|---|---|
| Initial state | 0 | 0 | Q has never been accessed | |
| T₁ (TS=100) reads Q | 100 | 0 | 100 | R-TS updated to 100 |
| T₂ (TS=150) reads Q | 150 | 0 | 150 | R-TS updated to 150 |
| T₃ (TS=200) writes Q | 200 | 200 | 200 | Both timestamps updated |
| T₄ (TS=180) wants to write Q | 180 | 200 | 200 | ⚠️ REJECTED: TS(T₄) < W-TS(Q) |
| T₅ (TS=250) reads Q | 250 | 200 | 250 | R-TS updated to 250 |
Every data item needs two additional timestamp fields. For a table with 1 billion rows and 8-byte timestamps, that's 16 GB of overhead. In practice, this metadata is stored with the tuple in the buffer pool and may be compressed or approximated (e.g., stored at page level instead of row level) to reduce overhead.
The relationship between timestamp assignment and conflict detection is the core of timestamp ordering. Let's trace through how these interact.
The Fundamental Principle:
A transaction T with timestamp TS(T) should see the database as if all transactions with smaller timestamps have completed, and no transactions with larger timestamps have started. Any operation that violates this is rejected.
For Reads:
When T wants to read data item Q:
For Writes:
When T wants to write data item Q:
Visualizing the Conflict:
Consider this timeline:
Timeline: TS=100 TS=150 TS=200
T1 begins T2 begins T3 begins
| |
| T2 writes Q (W-TS=150)
| |
T1 wants to write Q
TS(T1)=100 < W-TS(Q)=150 → ABORT!
T1 "should have" written Q before T2. But T2 already wrote. If we let T1 write now, the final value of Q would be T1's value—as if T1 happened last. This violates the timestamp order.
The Abort-Restart Loop:
When a transaction is aborted:
This loop may repeat if another conflict occurs, leading to potential starvation (discussed later).
Conflicts are detected on each operation, not at commit time. This 'eager' validation means wasted work is minimized—a transaction that will eventually fail due to a conflict is aborted as soon as that conflict becomes apparent, not after doing all its work. This is fundamentally different from optimistic protocols that defer all validation to commit.
Different timestamp assignment policies offer different trade-offs. Let's analyze them systematically.
Begin-Time Assignment Trade-offs:
First-Operation Assignment:
Advantages:
Disadvantages:
Commit-Time Assignment (Optimistic):
Advantages:
Disadvantages:
High-contention workloads favor begin-time assignment with early abort—fail fast, don't waste work. Low-contention, read-heavy workloads may benefit from commit-time assignment—most transactions succeed, so optimism is rewarded. Analyze your workload's conflict rate to choose appropriately.
Begin-time timestamp assignment creates a particular challenge for long-running transactions: they receive an "old" timestamp and face increasing conflict probability as time passes.
The Long Transaction Problem:
Consider:
Result: Long transactions face extremely high abort rates, potentially never completing.
Mitigation Strategies:
1. Transaction Splitting: Break long transactions into shorter ones:
2. Priority Timestamps: Assign priority based on transaction age or importance:
3. Timestamp Advancement: Allow transactions to "update" their timestamp under specific conditions:
4. Separate Classes: Run long and short transactions in separate isolation contexts:
Databases serving both long analytical queries and short OLTP transactions face inherent tension in timestamp ordering. This is one reason MVCC (Multi-Version Concurrency Control) became popular: readers never block writers, and long reads see consistent snapshots without conflicting with concurrent writes. We'll explore MVCC later in this chapter.
Implementing timestamp assignment correctly requires attention to several practical details.
Atomicity of Assignment:
Timestamp generation and transaction context creation should be atomic or carefully ordered:
// WRONG: Gap between timestamp and registration
ts = generate_timestamp() // TS = 500
// <-- Another thread could see TS 500 is "used" but find no transaction
registration = create_txn(ts)
// RIGHT: Atomic registration
with global_lock:
ts = generate_timestamp()
registration = create_txn(ts)
active_transactions.add(registration)
Without atomicity, a window exists where the timestamp is "consumed" but the transaction isn't yet visible to conflict detection.
Timestamp Visibility:
Other transactions and the conflict detection mechanism must be able to:
Range Considerations:
Timestamp values must have sufficient range:
Wraparound handling is complex; prefer larger ranges.
Clock Granularity (if clock-based):
If using system time:
Real database timestamp systems have many additional concerns: recovery (timestamps must survive crashes), replication (timestamps must be consistent across replicas), and observability (logging which timestamps were assigned when). The basic concepts we've covered form the foundation, but production implementations layer significant engineering on top.
We've thoroughly examined timestamp assignment—when, where, and how transactions receive their ordering identifier. Let's consolidate the key insights:
What's Next:
With timestamps assigned to transactions and maintained on data items, we can now examine how these timestamps create a transaction ordering. The next page explores how timestamp ordering establishes a serialization order and how this order is enforced through the protocol's read and write rules.
You now understand timestamp assignment—the critical moment when a transaction's logical position is defined. From assignment policies through data item timestamps to conflict detection integration, you can analyze how assignment choices affect system behavior. Next, we'll see how these timestamps create and enforce transaction ordering.