「僅當記錄不存在時插入」是極高頻業務需求:用戶註冊去重、幂等寫入、配置項初始化、日誌去重。MySQL 提供四種原生方案:**INSERT IGNORE、ON DUPLICATE KEY UPDATE、REPLACE INTO、INSERT ... SELECT ... WHERE NOT EXISTS**。本文深度對比四者的語義差異、性能特徵、副作用 (自增 ID 消耗、觸發器觸發、複製行為)、死鎖風險及選型決策樹。
| 方案 | 語法 | 衝突判斷依據 | 衝突時行為 | 自增ID消耗 | 觸發器 | 返回值 |
|---|---|---|---|---|---|---|
| INSERT IGNORE | INSERT IGNORE INTO t (...) VALUES (...) | 主鍵/唯一鍵衝突 | **靜默忽略**,不插入,不報錯,不更新 | **消耗** (預分配) | 觸發 BEFORE/AFTER INSERT (但未實際插入? 實測不觸發 AFTER INSERT) | 受影響行數: 0 |
| ON DUPLICATE KEY UPDATE (ODKU) | INSERT INTO t (...) VALUES (...) ON DUPLICATE KEY UPDATE col=VALUES(col) | 主鍵/唯一鍵衝突 | **更新**現有行 (可部分列、可用 VALUES()/OLD.) | **消耗** | 觸發 BEFORE/AFTER INSERT + BEFORE/AFTER UPDATE | 受影響行數: 1(新增) / 2(更新) |
| REPLACE INTO | REPLACE INTO t (...) VALUES (...) | 主鍵/唯一鍵衝突 | **刪除舊行 + 插入新行** (實際是 DELETE + INSERT) | **消耗** (舊行刪除、新行分配新ID) | 觸發 BEFORE/AFTER DELETE + BEFORE/AFTER INSERT | 受影響行數: 1(新增) / 2(刪+增) / >2(多唯一鍵衝突) |
| INSERT ... WHERE NOT EXISTS | INSERT INTO t (...) SELECT ... FROM dual WHERE NOT EXISTS (SELECT 1 FROM t WHERE pk=...) | 子查詢條件 (可任意複雜條件) | **不插入**,無錯誤,無更新 | **不消耗** (無衝突不分配) | 僅觸發 INSERT 觸發器 (若插入) | 受影響行數: 0/1 |
clients (client_id PK, name, type)-- 批量導入供應商為客戶,跳過已存在 client_id
INSERT IGNORE INTO clients (client_id, client_name, client_type)
SELECT supplier_id, supplier_name, 'advertising'
FROM suppliers;
-- 單條插入 (需配合 dual 或 VALUES)
INSERT IGNORE INTO clients (client_id, client_name, client_type)
VALUES (10345, 'IBM', 'advertising');
適用:日誌去重、幂等導入、不關心現有數據更新。**警告:會消耗自增 ID,大量忽略導致 ID 空洞暴增。**
-- 核心模式:衝突時更新指定列
INSERT INTO clients (client_id, client_name, client_type, updated_at)
VALUES (10345, 'IBM', 'advertising', NOW())
ON DUPLICATE KEY UPDATE
client_name = VALUES(client_name), -- 使用擬插入值
client_type = VALUES(client_type),
updated_at = NOW();
-- 進階:增量更新 (如計數器)
INSERT INTO stats (date, metric, count) VALUES ('2024-01-15', 'pv', 1)
ON DUPLICATE KEY UPDATE count = count + 1;
優勢:原子性 Upsert、可精確控制更新列、VALUES(col) 復用插入值、返回行數區分新增/更新 (1/2)。
REPLACE INTO clients (client_id, client_name, client_type)
VALUES (10345, 'IBM', 'advertising');
致命缺陷:
結論:生產環境嚴禁用於有自增主鍵/外鍵引用/多唯一索引的表。僅適用於無自增、單一唯一鍵、無觸發器、無外鍵的純數據字典表。
-- 單條插入 (dual 虛表)
INSERT INTO clients (client_id, client_name, client_type)
SELECT 10345, 'IBM', 'advertising'
FROM dual
WHERE NOT EXISTS (SELECT 1 FROM clients WHERE client_id = 10345);
-- 批量插入 (從源表同步)
INSERT INTO clients (client_id, client_name, client_type)
SELECT supplier_id, supplier_name, 'advertising'
FROM suppliers s
WHERE NOT EXISTS (SELECT 1 FROM clients c WHERE c.client_id = s.supplier_id);
優勢:無需唯一索引即可去重、不消耗自增 ID、條件靈活 (可加業務邏輯)。劣勢:并發下存在「檢查-插入」間隙競態 (Check-Then-Act),需應用層鎖或 Serializable 隔離級保證嚴格幂等。
事務 A、B 同時 INSERT ... ON DUPLICATE KEY UPDATE 同一 client_id:
緩解:
INSERT IGNORE + 後續單獨 UPDATE (兩步驟,非原子,但無死鎖)。事務 A、B 同時執行 INSERT ... WHERE NOT EXISTS,均查到不存在 → 同時插入 → **主鍵衝突報錯 (Duplicate entry)**。非原子操作。
解決:捕獲 Duplicate entry 錯誤碼 (1062) 重試,或升級為 ODKU (需唯一索引)。
| 方案 | 基於語句複製 (SBR) | 基於行複製 (RBR) | 混合模式 (MIXED) |
|---|---|---|---|
| INSERT IGNORE | 從庫重放可能不同步 (忽略錯誤行) | 安全 (記錄實際行變更) | 通常轉 RBR |
| ODKU | 從庫重放可能不同步 (VALUES() 函數語義差) | 安全 | 推薦 RBR |
| REPLACE | 風險極高 (刪除+插入順序、自增ID不同步) | 相對安全但自增ID仍可能漂移 | 極度不推薦 |
| NOT EXISTS | 子查詢在從庫重新執行,結果可能不同 (幻讀) | 安全 (記錄實際插入行) | 推薦 RBR |
鐵律:生產環境強制使用 ROW 格式 Binlog (binlog_format=ROW),消除 90% 複製不一致隱患。
START
│
├─ 是否有主鍵/唯一索引? → 否 → INSERT ... WHERE NOT EXISTS (捕獲 1062 重試) → END
│
├─ 是否需要衝突時更新數據? → 是 → ON DUPLICATE KEY UPDATE (首選) → END
│
├─ 是否純去重、不更新、極其在意自增ID不浪費? → 是 → INSERT ... WHERE NOT EXISTS (捕獲 1062 重試) → END
│
├─ 是否允許自增ID消耗、無外鍵、無觸發器、單唯一鍵? → 是 → INSERT IGNORE (簡單場景) → END
│
└─ 其他 → REPLACE INTO (極少數無自增/無FK/單UK/無觸發器的字典表) → END
ON DUPLICATE KEY UPDATE** (Upsert 標準解),配合 VALUES() 復用值。REPLACE INTO 用於業務核心表** (自增 ID、外鍵、觸發器、多 UK 皆為紅線)。INSERT ... WHERE NOT EXISTS + 捕獲 1062 重試。replica_skip_triggers=ON (如不需從庫觸發器)。Innodb_deadlocks、Questions、自增 ID 增長速率 (暴增提示 IGNORE/REPLACE 濫用)。「插入若不存在」看似簡單,實則涉及**並發控制、自增 ID 語義、觸發器副作用、複製一致性、索引依賴**等核心數據庫內核機制。ODKU 是 90% 場景的正解,其餘方案各有專用賽道。切記:**自增 ID 是寶貴資源,非必要不消耗;觸發器是副作用源頭,非必要不觸發;複製一致性是生命線,ROW 格式是底線。**