ON DUPLICATE KEY UPDATE 仅在主键或唯一索引冲突时触发更新;无唯一约束则不执行UPDATE,仅作普通INSERT;受影响行数为2表示更新、1为插入、0为值未变。

ON DUPLICATE KEY UPDATE 为什么只对唯一键/主键生效
它不是“检测数据是否已存在”,而是严格依赖索引冲突:只有当插入行导致 PRIMARY KEY 或 UNIQUE 索引重复时,才会触发 UPDATE 分支。普通字段(比如 email)没建唯一索引,哪怕值完全一样,也会直接报错或静默忽略(取决于 SQL 模式),绝不会走更新逻辑。
常见错误现象:INSERT INTO users (name, email) VALUES ('Alice', 'a@b.com') ON DUPLICATE KEY UPDATE name = VALUES(name); —— 如果 email 列没加 UNIQUE 约束,这条语句就只是普通插入,重复插入相同 email 会成功插入多条,根本不会更新。
- 必须提前在目标列上定义
UNIQUE或PRIMARY KEY(例如ALTER TABLE users ADD UNIQUE (email);) - 复合唯一索引也有效,比如
UNIQUE (user_id, type),此时只有两列组合重复才触发更新 - 如果表有多个唯一约束,只要任意一个冲突就会进入
UPDATE分支,不区分是哪个索引导致的
VALUES() 函数不是取变量,而是取本次 INSERT 的原始值
VALUES(col_name) 是 MySQL 特有的语法糖,代表“当前这条 INSERT 语句中为 col_name 指定的那个值”,不是变量、也不是子查询结果。它只在 ON DUPLICATE KEY UPDATE 子句里合法,其他地方用会报错。
典型误用:INSERT INTO logs (msg, level) VALUES ('err', 'warn') ON DUPLICATE KEY UPDATE level = 'error'; —— 这样写没问题;但若写成 level = VALUES('error') 就语法错误,因为 VALUES() 里面必须是列名,且该列得出现在前面的 VALUES 列表中。
-
VALUES(name)对应的是INSERT ... VALUES ('Alice', ...)里的第一个值,不是当前行原值,也不是新计算值 - 可以混用,比如
updated_at = NOW(), retry_count = VALUES(retry_count) + 1 - 不能在
VALUES()里写表达式,如VALUES(name || '_bak')不合法
影响行数返回值容易误解:1 表示插入,2 表示更新
执行后 mysql_affected_rows() 或客户端返回的“受影响行数”不是“变更了多少行”,而是行为标识:1 = 新增一行,2 = 更新了一行(即发生冲突后执行了 UPDATE)。0 表示什么都没做(比如主键冲突但 UPDATE 子句没真正修改任何字段值)。
这会导致业务逻辑误判。例如用这个值判断“是否首次注册”:如果用户已存在但只更新了 last_login,返回 2,但你误以为是“新增失败”,就可能跳过后续初始化逻辑。
- 不要依赖返回值判断“数据是否存在”,要用
SELECT显式查 - 如果 UPDATE 子句里所有赋值都等于当前值(比如
status = VALUES(status)且原值已等于新值),返回值是 0,不是 2 - 开启
sql_mode=STRICT_TRANS_TABLES时,若更新字段超出长度或类型限制,整个语句会失败,不返回 2
替代方案对比:REPLACE INTO 和 INSERT ... SELECT + WHERE NOT EXISTS
REPLACE INTO 看似更简单,但它本质是「DELETE + INSERT」:先删冲突行,再插新行。这会引发几个实际问题:自增 ID 跳变、外键级联删除风险、触发器执行两次、无法保留原行未指定字段的旧值。
而 INSERT ... SELECT ... WHERE NOT EXISTS 虽然能避免冲突,但需要显式检查、事务包裹、且并发下仍有竞态窗口(除非加锁),实用性远不如 ON DUPLICATE KEY UPDATE。
- 优先用
ON DUPLICATE KEY UPDATE,它是原子、轻量、意图明确的解决方案 - 仅当需要重置自增 ID 或强制刷新整行时才考虑
REPLACE INTO,并确认无外键和触发器副作用 - 高并发写同一键值时,
ON DUPLICATE KEY UPDATE内部会自动加行锁,比手写SELECT FOR UPDATE更简洁安全
最常被忽略的一点:UPDATE 子句里不能引用其他表,也不能用子查询更新自身(MySQL 8.0.19+ 支持部分相关子查询,但兼容性差),所有右值必须来自本语句的 VALUES() 或常量/函数。


















