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

UPSERT

Database PHP 5.0+ Intermediate
debt(d5/e3/b3/t7)
d5 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'specialist tool catches' (d5). The detection_hints specify semgrep and phpstan as tools that can catch the SELECT-then-INSERT race condition pattern, but this requires configuring specific rules — standard linters won't flag this by default, and the race condition itself only manifests under concurrent load.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). The quick_fix indicates replacing SELECT-then-INSERT with a single UPSERT statement (INSERT ... ON DUPLICATE KEY UPDATE or INSERT ... ON CONFLICT DO UPDATE). This is typically a localized change within a single repository method or query, but may require adjusting multiple call sites if the pattern is widespread.

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

Closest to 'localised tax' (b3). UPSERT is a query-level pattern choice that affects individual database operations. While applies_to shows it spans web/cli/queue contexts, each usage is independent — fixing one doesn't require fixing others, and the choice doesn't impose ongoing maintenance burden beyond that specific query.

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

Closest to 'serious trap' (t7). The misconception explicitly states developers believe REPLACE INTO is equivalent to INSERT ON CONFLICT, but REPLACE deletes and reinserts, resetting auto-increment IDs and losing columns. This contradicts intuition from other databases where 'replace' means 'update in place'. Additionally, common_mistakes show developers believe wrapping SELECT-then-INSERT in a transaction prevents races, which is false without serializable isolation.

About DEBT scoring →

Also Known As

INSERT ON CONFLICT ON DUPLICATE KEY UPDATE merge insert or update

TL;DR

An atomic INSERT-or-UPDATE operation — inserts the row if it does not exist, updates it if it does — eliminating the race condition of a separate check-then-insert.

Explanation

The naive SELECT-then-INSERT pattern has a race condition: two concurrent processes both see the row is missing and both try to insert, causing a unique constraint violation. UPSERT handles this atomically. PostgreSQL uses INSERT ... ON CONFLICT DO UPDATE. MySQL uses INSERT ... ON DUPLICATE KEY UPDATE or REPLACE INTO (which deletes then inserts — different semantics). Upserts are essential for idempotent event processing, counter increments, and sync operations.

Common Misconception

REPLACE INTO is equivalent to INSERT ON CONFLICT — REPLACE INTO deletes the old row and inserts a new one, resetting auto-increment IDs and losing any columns not in the INSERT list.

Why It Matters

The SELECT-then-INSERT pattern is a race condition that causes duplicate key errors under concurrent load — UPSERT eliminates this with a single atomic statement.

Common Mistakes

  • Using REPLACE INTO when ON DUPLICATE KEY UPDATE is needed — REPLACE deletes the row, triggering cascades and resetting IDs.
  • Not specifying which columns to update in ON CONFLICT — accidentally overwriting columns that should be preserved.
  • Using SELECT-then-INSERT in a transaction thinking it prevents races — without serialisable isolation, the race window still exists.
  • Not using EXCLUDED in PostgreSQL to reference the incoming row values in the UPDATE clause.

Code Examples

✗ Vulnerable
// Race condition — SELECT then INSERT:
$exists = $db->query('SELECT id FROM page_views WHERE page = ?', [$page])->fetch();
if ($exists) {
    $db->query('UPDATE page_views SET count = count + 1 WHERE page = ?', [$page]);
} else {
    $db->query('INSERT INTO page_views (page, count) VALUES (?, 1)', [$page]);
    // Two concurrent requests both see missing → both INSERT → duplicate key error
}
✓ Fixed
// PostgreSQL atomic upsert:
$db->query(
    'INSERT INTO page_views (page, count) VALUES (?, 1)
     ON CONFLICT (page) DO UPDATE SET count = page_views.count + 1',
    [$page]
);

// MySQL:
$db->query(
    'INSERT INTO page_views (page, count) VALUES (?, 1)
     ON DUPLICATE KEY UPDATE count = count + 1',
    [$page]
);

Added 15 Mar 2026
Edited 22 Mar 2026
Views 87
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings F 1 ping S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 2 pings S 0 pings S 1 ping M 1 ping T 2 pings W 1 ping T 0 pings F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 2 pings T 0 pings W 0 pings T 1 ping F
SEMrush 1
No pings yesterday
PetalBot 7 Amazonbot 6 Ahrefs 6 Perplexity 5 ChatGPT 5 Scrapy 5 Unknown AI 4 Google 3 Bing 3 Brave Search 3 Applebot 2 SEMrush 2 Majestic 1 Claude 1 Meta AI 1 Twitter/X 1
crawler 50 crawler_json 4 pre-tracking 1
🧱 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
🟠 High ⚙ Fix effort: Low
⚡ Quick Fix
Use INSERT ... ON DUPLICATE KEY UPDATE (MySQL) or INSERT ... ON CONFLICT DO UPDATE (PostgreSQL) for atomic upsert — never SELECT then INSERT/UPDATE, that has a race condition
📦 Applies To
PHP 5.0+ web cli queue-worker laravel
🔗 Prerequisites
🔍 Detection Hints
SELECT then INSERT or UPDATE pattern; race condition possible between check and insert; Laravel updateOrCreate() in high-concurrency context without unique index
Auto-detectable: ✓ Yes semgrep phpstan
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✓ Auto-fixable Fix: Low Context: Function Tests: Update


✓ schema.org compliant