Loading content...
Every society functions by rules—traffic flows because red light means stop and green means go. Databases have their own traffic rules for concurrent transactions, codified in the Lock Compatibility Matrix.
This matrix is the authoritative reference that the Lock Manager consults on every single lock request: Can this lock be granted given what locks already exist? Should this transaction wait, or can it proceed immediately? The matrix encodes, in a simple tabular form, the entire logic of lock coexistence.
Understanding the compatibility matrix is essential for:
In this page, we explore the compatibility matrix in depth: its structure, the reasoning behind each entry, extended matrices for additional lock modes, and how the matrix drives the Lock Manager's decisions.
By the end of this page, you will understand the structure of the basic S/X compatibility matrix, the logical reasoning behind each compatibility rule, extended matrices with intention locks and update locks, how the Lock Manager uses the matrix algorithmically, and performance implications of compatibility decisions.
The fundamental lock compatibility matrix for shared (S) and exclusive (X) locks is elegantly simple:
| Lock Request ↓ / Lock Held → | No Lock | Shared (S) | Exclusive (X) |
|---|---|---|---|
| Shared (S) | ✓ Grant | ✓ Grant | ✗ Wait |
| Exclusive (X) | ✓ Grant | ✗ Wait | ✗ Wait |
Reading the Matrix:
Let's analyze each cell:
The Fundamental Insight:
The matrix captures a single, profound insight: read operations are inherently compatible with each other, but write operations conflict with everything. This reflects the reality that:
Notice that the matrix is symmetric: S+X and X+S both result in waiting. This makes intuitive sense—if readers block writers, writers should block readers. The only asymmetric behavior is around the No Lock column, and even that's symmetric in a sense: both S and X can be granted on unlocked items.
The compatibility matrix can be formalized mathematically, which enables rigorous proofs about locking protocols and helps in designing new lock modes.
Formal Definition:
Let L be the set of lock modes. The compatibility relation ≈ is a binary relation on L:
For lock modes m₁, m₂ ∈ L, we say m₁ ≈ m₂ (m₁ is compatible with m₂) if and only if transactions holding m₁ and m₂ simultaneously on the same data item can execute correctly without conflict.
For the basic S/X model:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// Formal Representation of Lock Compatibility enum LockMode { NONE = 0, SHARED = 1, EXCLUSIVE = 2} // Compatibility matrix as a 2D array// compatible[held][requested] = true if compatibleconst compatibilityMatrix: boolean[][] = [ // NONE SHARED EXCLUSIVE /* NONE */ [true, true, true], /* SHARED */ [true, true, false], /* EXCL */ [true, false, false]]; /** * Determines if a requested lock is compatible with currently held lock */function isCompatible(held: LockMode, requested: LockMode): boolean { return compatibilityMatrix[held][requested];} // Mathematical properties of the compatibility relation /** * Reflexivity: Is every lock mode compatible with itself? * S ≈ S: true (multiple readers OK) * X ≈ X: false (only one writer) * Therefore, the relation is NOT reflexive */function isReflexive(): boolean { return Object.values(LockMode) .filter(m => typeof m === 'number') .every(m => isCompatible(m as LockMode, m as LockMode)); // Returns false because X ≉ X} /** * Symmetry: If m₁ ≈ m₂, does m₂ ≈ m₁? * For S/X: Yes, the matrix is symmetric * S ≈ S implies S ≈ S ✓ * S ≉ X implies X ≉ S ✓ */function isSymmetric(): boolean { for (let m1 = 0; m1 <= 2; m1++) { for (let m2 = 0; m2 <= 2; m2++) { if (compatibilityMatrix[m1][m2] !== compatibilityMatrix[m2][m1]) { return false; } } } return true; // Returns true for S/X matrix} /** * Lock mode strength ordering: X > S > None * A stronger lock is compatible with fewer other locks */function lockStrength(mode: LockMode): number { // Count how many modes this is incompatible with let conflicts = 0; for (let other = 0; other <= 2; other++) { if (!isCompatible(mode, other as LockMode)) conflicts++; } return conflicts;}// lockStrength(S) = 1 (conflicts with X only)// lockStrength(X) = 2 (conflicts with S and X)The mathematical formalization enables proofs of correctness for locking protocols. For example, we can prove that if all transactions follow Two-Phase Locking with this compatibility matrix, the resulting schedule is serializable. These proofs underpin database correctness guarantees.
The Lock Manager uses the compatibility matrix at the heart of its decision-making process. Here's how it processes each lock request:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
// Lock Manager Core Algorithm Using Compatibility Matrix interface LockRequest { transactionId: string; dataItem: string; mode: 'S' | 'X';} interface LockTableEntry { mode: 'NONE' | 'S' | 'X'; holders: Set<string>; // Transaction IDs waitQueue: LockRequest[];} class LockManager { private lockTable: Map<string, LockTableEntry> = new Map(); // Core compatibility check private compatibilityMatrix = { 'NONE': { 'S': true, 'X': true }, 'S': { 'S': true, 'X': false }, 'X': { 'S': false, 'X': false } }; /** * Main entry point for lock requests */ requestLock(request: LockRequest): 'GRANTED' | 'WAITING' { const entry = this.getOrCreateEntry(request.dataItem); // Step 1: Check if item is unlocked if (entry.mode === 'NONE') { return this.grantLock(request, entry); } // Step 2: Check wait queue (fairness) if (entry.waitQueue.length > 0) { // Someone already waiting - must queue to maintain fairness return this.enqueueLock(request, entry); } // Step 3: Consult compatibility matrix const isCompatible = this.compatibilityMatrix[entry.mode][request.mode]; if (isCompatible) { return this.grantLock(request, entry); } else { return this.enqueueLock(request, entry); } } private grantLock(request: LockRequest, entry: LockTableEntry): 'GRANTED' { // Update lock mode (S stays S if adding S, becomes S if was NONE) if (request.mode === 'X') { entry.mode = 'X'; } else if (entry.mode === 'NONE') { entry.mode = 'S'; } // If entry.mode was already 'S' and request is 'S', stays 'S' entry.holders.add(request.transactionId); console.log(`Lock GRANTED: ${request.transactionId} gets ${request.mode} on ${request.dataItem}`); return 'GRANTED'; } private enqueueLock(request: LockRequest, entry: LockTableEntry): 'WAITING' { entry.waitQueue.push(request); console.log(`Lock WAITING: ${request.transactionId} queued for ${request.mode} on ${request.dataItem}`); return 'WAITING'; } /** * Release lock and process wait queue */ releaseLock(transactionId: string, dataItem: string): void { const entry = this.lockTable.get(dataItem); if (!entry) return; entry.holders.delete(transactionId); // Update mode if no holders left if (entry.holders.size === 0) { entry.mode = 'NONE'; } // Process waiting transactions this.processWaitQueue(entry, dataItem); } private processWaitQueue(entry: LockTableEntry, dataItem: string): void { if (entry.waitQueue.length === 0) return; // Try to grant as many compatible requests as possible const toGrant: LockRequest[] = []; const remaining: LockRequest[] = []; for (const request of entry.waitQueue) { // Would this request be compatible with what we'd have after granting toGrant? const futureMode = this.getFutureMode(entry.mode, toGrant); if (this.compatibilityMatrix[futureMode][request.mode]) { toGrant.push(request); } else { // Once we hit an incompatible request, stop (FIFO fairness) remaining.push(...entry.waitQueue.slice(entry.waitQueue.indexOf(request))); break; } } entry.waitQueue = remaining; // Grant all compatible requests for (const request of toGrant) { this.grantLock(request, entry); // Wake up transaction (implementation-dependent) } } private getFutureMode(current: string, toGrant: LockRequest[]): 'NONE' | 'S' | 'X' { if (toGrant.some(r => r.mode === 'X')) return 'X'; if (toGrant.length > 0 || current === 'S') return 'S'; return 'NONE'; }}When an X-lock is released, the Lock Manager can grant multiple waiting S-locks at once (since S is compatible with S). This is more efficient than processing them one at a time and is a key optimization in real systems.
The basic S/X model, while foundational, is often extended with additional lock modes to handle special cases more efficiently. The most common extensions are Update Locks (U) and Intention Locks (IS, IX, SIX).
Update Locks (U) solve the upgrade deadlock problem we saw earlier. A U-lock allows reading with the intention to write later.
The Problem:
T1: S-lock(X) → T2: S-lock(X) → T1: wants X-lock (blocked by T2)
→ T2: wants X-lock (blocked by T1)
= DEADLOCK
The Solution:
T1: U-lock(X) → T2: S-lock(X) blocked! → T1: upgrade to X-lock (succeeds)
| Request \ Held | S | U | X |
|---|---|---|---|
| S | ✓ | ✓ | ✗ |
| U | ✓ | ✗ | ✗ |
| X | ✗ | ✗ | ✗ |
Each new lock mode adds a row AND column to the matrix. With 6 modes (IS, IX, S, SIX, U, X), the matrix has 36 cells. This is why most systems use only the modes they truly need—complexity grows quadratically.
Intention locks enable multiple-granularity locking (MGL), where transactions can lock at different levels of the database hierarchy (database → table → page → row).
The Hierarchy:
Database
└── Table A
├── Page 1
│ ├── Row 1
│ ├── Row 2
│ └── Row 3
└── Page 2
├── Row 4
└── Row 5
MGL Rules:
123456789101112131415161718192021222324252627282930
-- Multiple-Granularity Locking in Practice -- Transaction T1: Update a single row-- Internally acquires: IX(Table) → IX(Page) → X(Row)BEGIN TRANSACTION;UPDATE employees SET salary = 50000 WHERE id = 123;-- Only Row 123 is X-locked; other rows on same page are accessibleCOMMIT; -- Transaction T2: Scan entire table-- Internally acquires: S(Table)BEGIN TRANSACTION;SELECT * FROM employees;-- If T1 is running, T2 blocks (IX not compatible with S)COMMIT; -- Transaction T3: Update different row (same table, different page)-- Internally acquires: IX(Table) → IX(Page 2) → X(Row)BEGIN TRANSACTION;UPDATE employees SET salary = 60000 WHERE id = 456;-- T1 and T3 can RUN CONCURRENTLY if rows are on different pages-- (IX is compatible with IX at table level)COMMIT; -- SQL Server: Explicit table lock (bypasses row locking)SELECT * FROM employees WITH (TABLOCK, HOLDLOCK);-- Acquires S directly on table - blocks all writersWithout intention locks, checking if a table can be X-locked would require examining every row's lock. With intention locks, we check one table-level lock. This changes O(n) checks to O(1)—essential for scalability.
The compatibility matrix directly impacts system throughput. Understanding these implications helps in designing efficient concurrent applications.
| Workload Type | Lock Pattern | Compatibility Behavior | Performance |
|---|---|---|---|
| Read-Heavy | Many S, few X | S-locks rarely block each other | Excellent concurrency |
| Write-Heavy | Many X, few S | X-locks serialize writes | Poor concurrency on hot rows |
| Mixed Read-Write | S and X interleaved | Writers wait for readers and vice versa | Moderate concurrency |
| Hot Spot | Many X on one row | All writers queue on single item | Serial execution effectively |
Mitigation Strategies:
Production databases expose lock wait statistics. High lock wait times on specific tables/rows indicate contention. Use this data to identify hot spots and optimize access patterns.
Different database systems implement variations of the compatibility matrix. Understanding these variations helps when working with specific systems.
PostgreSQL uses eight table-level lock modes:
PostgreSQL primarily uses MVCC for reads, so the row-level locking is mainly for explicit FOR UPDATE/SHARE and for handling UPDATE/DELETE conflicts. Most SELECT statements don't acquire any row locks.
While the fundamental S/X compatibility principles are universal, the details vary significantly between systems. Always consult your specific database's documentation for lock modes and their exact compatibility rules.
The lock compatibility matrix is the authoritative rulebook that governs all lock decisions. Let's consolidate the key insights:
What's Next:
With a solid understanding of lock concepts, types, and compatibility, we'll examine Lock Granularity—the art of choosing what to lock. Should we lock an entire table, a page, or a single row? The answer profoundly affects both correctness and performance.
You now understand the lock compatibility matrix: its structure, mathematical properties, extended lock modes, and how the Lock Manager uses it algorithmically. In the next page, we'll explore lock granularity and the tradeoffs involved in choosing what to lock.