INSERT INTO tbl (pk_col, col1, col2) VALUES (v1, v2, v3)
ON DUPLICATE KEY UPDATE col1 = NEW_VALUE, col2 = col2 + 1;
前提:表必須有 PRIMARY KEY 或 UNIQUE INDEX,觸發衝突時轉為更新。
| 場景 | SQL | 效果等價於 |
|---|---|---|
| 單行衝突,增量更新 | INSERT INTO t(a,b,c) VALUES(1,2,3) ON DUPLICATE KEY UPDATE c=c+1, b=b-1; | UPDATE t SET c=c+1, b=b-1 WHERE a=1; |
| 多行批量,取 VALUES() | INSERT INTO t(a,c) VALUES(1,3),(1,7) ON DUPLICATE KEY UPDATE c=VALUES(c); | 衝突行 c=7(第二行值),非衝突行 c=3 |
| 多行批量,增量更新 | INSERT INTO t(a,c) VALUES(1,3),(1,7) ON DUPLICATE KEY UPDATE c=c+1; | 衝突行 c=4(原值 3+1),忽略第二行值 |
ON DUPLICATE KEY UPDATE 子句中可用。INSERT ... VALUES 列表中對應列的**當前行值**;多行批量時,每行各自取自己的值。WHERE、SELECT 列表中使用。CREATE TABLE user_login_stat (
user_id BIGINT NOT NULL PRIMARY KEY,
login_count INT NOT NULL DEFAULT 0,
last_login DATETIME NOT NULL,
last_ip VARCHAR(45) NOT NULL,
UNIQUE KEY uk_user (user_id)
);
-- 每次登錄調用
INSERT INTO user_login_stat (user_id, login_count, last_login, last_ip)
VALUES (?, 1, NOW(), ?)
ON DUPLICATE KEY UPDATE
login_count = login_count + 1,
last_login = VALUES(last_login),
last_ip = VALUES(last_ip);
單條 SQL 完成「首次插入 / 非首次累加計數 + 更新時間 IP」,無需應用層 SELECT → IF → INSERT/UPDATE 往返。
INSERT INTO log (type, content) VALUES ('login', '...')
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id);
SELECT LAST_INSERT_ID(); -- 返回插入行 ID 或衝突行 ID
適用於「唯一鍵非自增主鍵」場景,需主鍵為自增列。
X 鎖,非衝突插入加 INSERT INTENTION 鎖。ON DUPLICATE KEY UPDATE 多行時,不同事務鎖獲取順序不一致易死鎖 → 應用層統一按主鍵排序後批量寫入。| 錯誤做法 | 後果 | 正確做法 |
|---|---|---|
| 表無 PK/UK | 語法報錯 ERROR 1138 | 先加唯一索引 |
| UPDATE 子句引用未列出的列 | 報錯 Unknown column | INSERT 列表包含所有需更新的列,或用子查詢 |
| VALUES(col) 用在非衝突行 | 返回 NULL | 理解:僅衝突行才取 VALUES() |
| 作為標準 SQL 移植 | PostgreSQL 用 ON CONFLICT,Oracle 用 MERGE | 封裝 DAL 層適配不同方言 |
ON DUPLICATE KEY UPDATE 是 MySQL 特有的「Upsert」語法,極大簡化「存在即更新、不存在即插入」邏輯,配合 VALUES() 靈活控制衝突行取值策略,是高併發寫入場景(計數器、統計、冪等導入)的利器。