← CodeClarityLab Home
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 21
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 1 ping S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F
No pings yet today
No pings yesterday
Amazonbot 6 Perplexity 5 Unknown AI 3 Google 2 Ahrefs 2 Majestic 1
crawler 17 crawler_json 1 pre-tracking 1
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