INSERT ... ON DUPLICATE KEY UPDATE (ODKU) 是 MySQL 實現「有則更新、無則插入」的原子操作,廣泛用於計數器、同步任務、冪等寫入。本文聚焦核心語法、VALUES() 函數妙用、多唯一索引行為邊界與生產級優化模式。
INSERT INTO table (pk, col1, col2) VALUES (v_pk, v1, v2)
ON DUPLICATE KEY UPDATE col1 = v1_new, col2 = v2_new;
1 新插入,2 更新現有,0 (極罕見 IGNORE 組合)。在 UPDATE 子句中,VALUES(col_name) 返回該行 VALUES 列表中對應的值,而非表中舊值。
INSERT INTO counters (date, type, cnt) VALUES ('2024-01-15', 'pv', 1)
ON DUPLICATE KEY UPDATE cnt = cnt + VALUES(cnt); -- cnt = cnt + 1
INSERT INTO daily_stats (stat_date, metric, val) VALUES
('2024-01-15', 'pv', 100),
('2024-01-15', 'uv', 50),
('2024-01-15', 'order', 10)
ON DUPLICATE KEY UPDATE val = val + VALUES(val);
等效於分別執行:
UPDATE daily_stats SET val = val + 100 WHERE stat_date='2024-01-15' AND metric='pv';
UPDATE daily_stats SET val = val + 50 WHERE stat_date='2024-01-15' AND metric='uv';
UPDATE daily_stats SET val = val + 10 WHERE stat_date='2024-01-15' AND metric='order';
單語句、單次網絡往返、原子性 —— 高併發計數器首選。
CREATE TABLE t (
a INT PRIMARY KEY,
b INT UNIQUE,
c INT
);
INSERT INTO t (a,b,c) VALUES (1, 10, 100)
ON DUPLICATE KEY UPDATE c = c + 1;
a=1 → 更新該行 c+1 (預期內)。a=1 但有 b=10 → 更新 b=10 那行,a 保持原值,c+1。a=1 和 b=10 (不同行) → 只更新第一個匹配到的行 (非確定性,取決於優化器訪問順序)。| 組合 | 實際行為 |
|---|---|
| INSERT IGNORE ... ODKU | 衝突時執行 UPDATE (ODKU 優先),IGNORE 不生效 |
| INSERT DELAYED ... ODKU | DELAYED 被忽略,退化為同步 INSERT + ODKU |
-- 唯一鍵 (date, metric) + 更新列 val
ALTER TABLE daily_stats ADD UNIQUE uk_date_metric (stat_date, metric);
-- ODKU 可直接在索引頁完成更新,無需聚簇索引回表
max_allowed_packet (默認 4MB/64MB) 會報錯。# 應用層解析
if affected == 1: metrics.counter('upsert.insert')
elif affected == 2: metrics.counter('upsert.update')
ODKU 觸發 BEFORE UPDATE / AFTER UPDATE。若觸發器內再操作同表,可能死鎖/遞歸。建議:核心計數器表不掛觸發器,異步通道 (Binlog/CDC) 處理下游。
MySQL 尚未支持標準 SQL ON CONFLICT DO UPDATE (PostgreSQL/SQLite 語法)。ODKU 仍是唯一原生 Upsert 方式。
ODKU = 原子 Upsert + 批量計數器聚合 + VALUES() 引用插入值。掌握 VALUES(col)、多唯一索引非確定性、組合互斥規則、覆蓋索引優化,即可在高併發寫入鏈路中寫出正確、高效、可觀測的冪等邏輯。生產原則:單表單一業務唯一鍵 + 批量 ODKU + 異步下游。