萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 資料庫 >> MySQL 教學

MySQL「插入若不存在」模式完整指南:INSERT IGNORE、ON DUPLICATE KEY、REPLACE INTO、NOT EXISTS 對比與選型

「僅當記錄不存在時插入」是極高頻業務需求:用戶註冊去重、幂等寫入、配置項初始化、日誌去重。MySQL 提供四種原生方案:**INSERT IGNORE、ON DUPLICATE KEY UPDATE、REPLACE INTO、INSERT ... SELECT ... WHERE NOT EXISTS**。本文深度對比四者的語義差異、性能特徵、副作用 (自增 ID 消耗、觸發器觸發、複製行為)、死鎖風險及選型決策樹。

一、四大方案語義對比表

方案語法衝突判斷依據衝突時行為自增ID消耗觸發器返回值
INSERT IGNOREINSERT 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 INTOREPLACE INTO t (...) VALUES (...)主鍵/唯一鍵衝突**刪除舊行 + 插入新行** (實際是 DELETE + INSERT)**消耗** (舊行刪除、新行分配新ID)觸發 BEFORE/AFTER DELETE + BEFORE/AFTER INSERT受影響行數: 1(新增) / 2(刪+增) / >2(多唯一鍵衝突)
INSERT ... WHERE NOT EXISTSINSERT INTO t (...) SELECT ... FROM dual WHERE NOT EXISTS (SELECT 1 FROM t WHERE pk=...)子查詢條件 (可任意複雜條件)**不插入**,無錯誤,無更新**不消耗** (無衝突不分配)僅觸發 INSERT 觸發器 (若插入)受影響行數: 0/1

二、實戰代碼示例

場景:客戶表 clients (client_id PK, name, type)

1. INSERT IGNORE:簡單去重,不關心更新

-- 批量導入供應商為客戶,跳過已存在 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 空洞暴增。**

2. ON DUPLICATE KEY UPDATE:Upsert (更新或插入) 標準解法

-- 核心模式:衝突時更新指定列
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)。

3. REPLACE INTO:暴力替換 (慎用!)

REPLACE INTO clients (client_id, client_name, client_type)
VALUES (10345, 'IBM', 'advertising');

致命缺陷:

  1. 自增 ID 重置:衝突時刪除舊行 (含主鍵),插入新行分配**新自增 ID**,破壞引用完整性 (外鍵、應用層緩存 ID)。
  2. 觸發器雙觸發:DELETE + INSERT 觸發器鏈,副作用雙倍。
  3. 多唯一鍵衝突刪除多行:若表有多個唯一索引,新行同時衝突多個舊行,**全部刪除**,數據丟失風險極高。
  4. 性能差:兩次操作 + 索引維護開銷雙倍。

結論:生產環境嚴禁用於有自增主鍵/外鍵引用/多唯一索引的表。僅適用於無自增、單一唯一鍵、無觸發器、無外鍵的純數據字典表。

4. INSERT ... WHERE NOT EXISTS:無唯一索引也能用、不消耗自增 ID

-- 單條插入 (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 隔離級保證嚴格幂等。

三、並發場景深度分析:死鎖與競態

1. ODKU 死鎖風險 (高併發寫入同一鍵)

事務 A、B 同時 INSERT ... ON DUPLICATE KEY UPDATE 同一 client_id:

  • InnoDB 鎖機制:先嘗試插入,需獲取**插入意向鎖 (Insert Intention Lock)** + **記錄鎖 (Record Lock)**。
  • 若記錄不存在,雙方獲取插入意向鎖 (兼容),但隨後需獲取記錄鎖 (排他) → **死鎖** 概率極高。
  • 若記錄已存在,直接獲取記錄鎖 (排他),先到者更新,後者等待 → **無死鎖,但有鎖等待**。

緩解:

  • 應用層分布式鎖 (Redis Redlock) 序列化同一鍵寫入。
  • 將高併發鍵拆分為多個桶 (Sharding),分散鎖競爭。
  • 使用 INSERT IGNORE + 後續單獨 UPDATE (兩步驟,非原子,但無死鎖)。

2. NOT EXISTS 競態條件

事務 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 皆為紅線)。
  • ✅ **高併發熱點鍵**:應用層分布式鎖 + ODKU,或分桶降低衝突率。
  • ✅ **無唯一索引表去重**:INSERT ... WHERE NOT EXISTS + 捕獲 1062 重試。
  • ✅ **Binlog 必須 ROW 格式**,從庫 replica_skip_triggers=ON (如不需從庫觸發器)。
  • ✅ **監控指標**:Innodb_deadlocks、Questions、自增 ID 增長速率 (暴增提示 IGNORE/REPLACE 濫用)。

結語

「插入若不存在」看似簡單,實則涉及**並發控制、自增 ID 語義、觸發器副作用、複製一致性、索引依賴**等核心數據庫內核機制。ODKU 是 90% 場景的正解,其餘方案各有專用賽道。切記:**自增 ID 是寶貴資源,非必要不消耗;觸發器是副作用源頭,非必要不觸發;複製一致性是生命線,ROW 格式是底線。**

copyright © 萬盛學電腦網 all rights reserved