`INSERT ... ON DUPLICATE KEY UPDATE` (ODKU) 是 MySQL 獨有的「Upsert」語法,能在主鍵/唯一鍵衝突時自動轉為更新,極大簡化「不存在則插入,存在則更新」的業務邏輯。本文系統解析語法細節、進階用法、效能特性與避坑指南。
INSERT INTO table_name (col1, col2, col3)
VALUES (val1, val2, val3)
ON DUPLICATE KEY UPDATE
col2 = VALUES(col2),
col3 = col3 + 1;
在 `UPDATE` 子句中,`VALUES(col_name)` 返回 **原本準備插入的值**,而非資料庫現有值。這是構建「插入值參與更新運算」的關鍵。
-- 統計每日 UV:不存在插入 1,存在則 +1
INSERT INTO daily_uv (date, uv) VALUES ('2024-01-15', 1)
ON DUPLICATE KEY UPDATE uv = uv + 1;
-- 等價於 (但需兩次查詢)
INSERT INTO daily_uv (date, uv) VALUES ('2024-01-15', 1)
ON DUPLICATE KEY UPDATE uv = VALUES(uv) + 1; -- VALUES(uv)=1
INSERT INTO user_stats (user_id, login_count, last_login)
VALUES
(1001, 1, '2024-01-15 08:30:00'),
(1002, 1, '2024-01-15 09:15:00'),
(1003, 1, '2024-01-15 10:00:00')
ON DUPLICATE KEY UPDATE
login_count = login_count + VALUES(login_count), -- 累加
last_login = VALUES(last_login); -- 覆蓋為最新時間
**效能優勢**:相比應用層 `SELECT` 判斷再 `INSERT`/`UPDATE`,ODKU 減少網路往返、鎖競爭窗口、解析優化開銷,批次時效能提升 **10x-100x**。
INSERT INTO products (sku, price, stock, version)
VALUES ('SKU001', 99.00, 10, 1)
ON DUPLICATE KEY UPDATE
price = VALUES(price),
stock = stock + VALUES(stock),
version = version + 1;
INSERT INTO logs (msg, created_at) VALUES ('test', NOW())
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), msg = VALUES(msg);
SELECT LAST_INSERT_ID(); -- 返回插入時的自增 ID,或更新時該行的 ID
**關鍵**:`ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)` 讓更新分支也能讓 `LAST_INSERT_ID()` 返回該行 ID,否則更新時返回 0。
| 語法 | 衝突行為 | 返回值 | 適用場景 |
|---|---|---|---|
| INSERT IGNORE | 靜默忽略,不插入、不更新、不報錯 | affected_rows=0 | 冪寫入、去重、不關心現有值 |
| ON DUPLICATE KEY UPDATE | 執行更新 | 插入=1,更新=2 | 需更新計數器、時間戳、版本號 |
| REPLACE INTO | 刪除舊行 + 插入新行 | 刪除+插入=2 | 極少用,**會觸發 FK 級聯刪除、自增 ID 變更、觸發器雙重觸發** |
標準 SQL `MERGE INTO target USING source ON ... WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ...` 功能更強 (支援多表、複雜條件),但 MySQL 至今未實作。ODKU 是 MySQL 的輕量替代方案,語法簡潔、效能高,覆蓋 95% Upsert 場景。
function upsertBatch(table, uniqueKeys, rows, updateCols) {
// rows: [{col1:v, col2:v...}, ...]
// uniqueKeys: ['uk1', 'uk2'] 用於應用層排序
// updateCols: 需更新的欄位 (含累加邏輯)
rows.sort((a,b) => compareByKeys(a,b,uniqueKeys)); // 關鍵:避免死鎖
const chunks = chunk(rows, 500);
for (const batch of chunks) {
const cols = Object.keys(batch[0]);
const placeholders = batch.map(() => '(' + cols.map(() => '?').join(',') + ')').join(',');
const updates = updateCols.map(c => `${c} = ${c} + VALUES(${c})`).join(', ');
const sql = `INSERT INTO ${table} (${cols.join(',')}) VALUES ${placeholders} ON DUPLICATE KEY UPDATE ${updates}`;
const params = batch.flatMap(r => cols.map(c => r[c]));
await db.execute(sql, params);
}
}
`ON DUPLICATE KEY UPDATE` 是 MySQL 開發者工具箱裡的「瑞士軍刀」。掌握其語法細節、鎖行為、批次優化與避坑指南,能讓高併發寫入邏輯既簡潔又強健。