← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

SELECT FOR UPDATE

Database Advanced
debt(d7/e3/b5/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate 'automated: no' and the code_pattern requires spotting a check-then-update without FOR UPDATE. No linter or SAST tool is listed. The race condition is silent under low concurrency and only manifests as corrupt data (negative stock, double-bookings) under production load — invisible until users hit the symptom.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). The quick_fix says: wrap SELECT ... FOR UPDATE inside beginTransaction()/commit() and index the WHERE clause. This is more than a single-line swap because it requires adding a transaction boundary and verifying index coverage, but it is localised to the one query/code path rather than a cross-cutting refactor.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). The choice applies to both web and cli contexts. Every developer working on inventory, balance, or reservation logic must understand pessimistic locking semantics, transaction scoping, and index discipline to avoid lock escalation. This shapes how concurrent write paths are designed but does not redefine the entire system architecture.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field states exactly this: developers believe a regular SELECT inside a transaction already prevents concurrent modification. This is the intuitive assumption — transactions feel like isolation — but without FOR UPDATE other transactions can freely modify rows between the SELECT and UPDATE. The 'obvious' pattern (begin transaction, SELECT, UPDATE) silently races, directly contradicting the expectation that transaction scope equals read lock.

About DEBT scoring →

Also Known As

SELECT FOR UPDATE MySQL pessimistic locking PHP locking read

TL;DR

A locking read that acquires exclusive row locks — preventing other transactions from modifying selected rows until commit.

Explanation

SELECT ... FOR UPDATE locks selected rows exclusively within a transaction. Other transactions attempting to modify or lock those rows block until the first transaction commits or rolls back. Used for 'check then update' patterns — e.g. reading inventory before decrementing. SELECT ... FOR SHARE acquires shared locks — multiple readers allowed, writers blocked. Requires an active beginTransaction(). Always index the WHERE clause or the lock may escalate to a table lock.

Watch Out

Always keep FOR UPDATE transactions as short as possible — long-held locks are the primary cause of deadlocks.

Common Misconception

A regular SELECT inside a transaction prevents concurrent modification. Without FOR UPDATE, other transactions can modify rows between your SELECT and UPDATE in the same transaction.

Why It Matters

Without locking reads, two concurrent transactions can both check stock=1, both proceed, and both decrement — creating negative stock. FOR UPDATE prevents this race condition structurally.

Common Mistakes

  • Using SELECT FOR UPDATE outside a transaction — has no locking effect.
  • Using an unindexed WHERE clause — can escalate to a full table lock blocking all writes.
  • Holding FOR UPDATE locks across slow operations — causes lock timeout errors under concurrency.

Avoid When

  • Avoid for read-heavy workflows — held locks block concurrent readers on the same rows.
  • Do not hold FOR UPDATE locks across user-facing waits or external API calls.

When To Use

  • Use when the gap between reading and writing a value must be race-condition free — inventory, account balances, seat reservations.
  • Use when the cost of conflict detection (optimistic locking retries) is too high.

Code Examples

✗ Vulnerable
// Race condition — two requests both read stock=1 and both proceed
$stock = $pdo->prepare('SELECT stock FROM products WHERE id = ?');
$stock->execute([$id]);
if ($stock->fetchColumn() >= $qty) {
    // Another request may decrement between this check and the UPDATE below
    $pdo->prepare('UPDATE products SET stock = stock - ? WHERE id = ?')->execute([$qty, $id]);
}
✓ Fixed
$pdo->beginTransaction();
try {
    $stmt = $pdo->prepare('SELECT stock FROM products WHERE id = ? FOR UPDATE');
    $stmt->execute([$productId]);
    $row = $stmt->fetch();
    if ($row['stock'] < $qty) { $pdo->rollBack(); throw new \RuntimeException('No stock'); }
    $pdo->prepare('UPDATE products SET stock = stock - ? WHERE id = ?')->execute([$qty, $productId]);
    $pdo->commit();
} catch (\Throwable $e) { $pdo->rollBack(); throw $e; }

Added 31 Mar 2026
Views 67
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 2 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 1 ping S 1 ping M 0 pings T 0 pings W 2 pings T 1 ping F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 2 pings T 2 pings F
Brave Search 1 Applebot 1
PetalBot 2
Scrapy 7 Perplexity 6 ChatGPT 6 Google 4 Ahrefs 4 Bing 3 PetalBot 3 Unknown AI 2 Meta AI 2 SEMrush 2 Majestic 1 Twitter/X 1 Brave Search 1 Applebot 1
crawler 41 crawler_json 2
🧱 FUNDAMENTALS — new to this? Start with the ground floor.
Database database A database is an organized collection of data stored electronically, designed so programs can efficiently retrieve, add, update, and delete information.

Nearly every application needs to remember information between sessions. Databases provide the reliable, fast, and organized storage that makes persistent data possible at any scale.

💡 Always use prepared statements with placeholders—never concatenate user input directly into database queries.

Ask Codex about Database →
DEV INTEL Tools & Severity
⚙ Fix effort: Medium
⚡ Quick Fix
Wrap SELECT ... FOR UPDATE inside beginTransaction() / commit() — always index the WHERE clause to avoid table-level lock escalation
📦 Applies To
web cli
🔗 Prerequisites
🔍 Detection Hints
check-then-update pattern without FOR UPDATE or transaction
Auto-detectable: ✗ No
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: Function Tests: Update


✓ schema.org compliant