ON DUPLICATE KEY UPDATE仅在主键或唯一索引冲突时触发更新,非“存在即更新”;必须有PRIMARY KEY或UNIQUE约束,普通索引无效;UPDATE中用VALUES(col)引用插入值,不可用WHERE,影响行数为1(插入)、2(更新且值变)或0(值未变)。

ON DUPLICATE KEY UPDATE 必须依赖唯一索引才能触发
没有 UNIQUE 或 PRIMARY KEY 索引,ON DUPLICATE KEY UPDATE 就不会生效——它不是靠“查一遍再决定”,而是靠插入时索引冲突报错后自动跳转分支。常见错误是建表时只加了普通索引,或联合唯一索引列顺序写反,导致冲突不被识别。
检查方式很简单:SHOW CREATE TABLE your_table; 确认输出里有类似 UNIQUE KEY `uk_email` (`email`) 或 UNIQUE KEY `uk_user_id_type` (`user_id`, `type`) 的定义。
- 联合唯一索引必须完整匹配:比如索引是
(a,b,c),只提供a和b不会触发冲突 - 主键冲突和唯一键冲突效果一致,但主键更新时不能改主键值(除非你显式在
UPDATE子句里赋新值,这其实是覆盖) - 如果业务上需要按非索引字段判断“是否重复”,得先用
INSERT IGNORE+UPDATE ... WHERE两步走,或者改用存储过程+异常捕获
VALUES() 函数不是取旧值,而是取本次 INSERT 尝试插入的值
VALUES(col_name) 是 MySQL 特有的语法糖,它指向的是当前这条 INSERT 语句中对应位置的值,不是原记录的旧值。这点极易误解,尤其在做累加操作时。
例如想对已有记录的 views 字段加 1:
INSERT INTO stats (user_id, views, updated_at) VALUES (123, 1, NOW()) ON DUPLICATE KEY UPDATE views = views + VALUES(views), updated_at = VALUES(updated_at);
这里 VALUES(views) 是 1,不是原记录的 views 值;所以等价于 views = views + 1,正确。但如果写成 views = VALUES(views),就变成直接覆盖为 1,丢了原有计数。
-
VALUES()只能在ON DUPLICATE KEY UPDATE子句中使用,不能用在SELECT或其他地方 - 不能用子查询替代,比如
updated_at = (SELECT NOW())会报ERROR 1093 - 若字段允许为
NULL,且你传入NULL,VALUES(col)就是NULL,更新后该字段也会变NULL
批量 VALUES 写法比 INSERT … SELECT 更可控、更常用
虽然 INSERT ... SELECT ... ON DUPLICATE KEY UPDATE 合法,但实际项目中更推荐直接拼多组 VALUES,尤其配合 MyBatis、JDBC Batch 或 ORM 批量接口。原因很实在:不用构造子查询别名、不担心 SELECT 来源表锁表、也不用处理字段顺序与目标表默认顺序不一致的问题。
典型安全写法:
INSERT INTO users (id, name, email, updated_at) VALUES (101, 'Alice', 'alice@example.com', NOW()), (102, 'Bob', 'bob@example.com', NOW()), (103, 'Charlie','charlie@example.com', NOW()) ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = VALUES(updated_at);
- MySQL 单条语句最多支持约 1000 组
VALUES(受max_allowed_packet和net_buffer_length影响),超量需分批 - 所有
VALUES行字段数必须严格一致,少一个逗号或空值占位都报ERROR 1136 - 如果表有自增主键但你不关心它的值,又希望冲突由唯一键(如
email)触发,则VALUES中可省略该主键列——但前提是没在INSERT INTO ...显式列出它;一旦列出,就必须给值
MyBatis 动态 SQL 中 分隔符和括号必须严格配对
用 MyBatis 做批量 upsert 时,<foreach> 生成多组 VALUES 是最常见场景,但也是出错高发区。关键点不在逻辑,而在标点符号是否闭合。
正确模板片段:
<insert id="batchUpsert">
INSERT INTO user_profile (uid, bio, avatar_url, updated_at)
VALUES
<foreach collection="list" item="u" separator=",">
(#{u.uid}, #{u.bio}, #{u.avatarUrl}, NOW())
</foreach>
ON DUPLICATE KEY UPDATE
bio = VALUES(bio),
avatar_url = VALUES(avatar_url),
updated_at = VALUES(updated_at)
</insert>
-
separator=","必须存在,否则生成的 SQL 缺少逗号,直接语法错误 - 每组
VALUES必须用圆括号包裹,且括号前后不能有多余空格或换行干扰解析(某些老版本驱动敏感) - 如果
collection为空列表,MyBatis 默认会跳过整个<foreach>,导致VALUES后面没内容而报错;建议在 Java 层提前判空,或用<bind>加兜底
VALUES() 是否被当成旧值、以及动态拼接时少了一个逗号或括号。这些地方一错,MySQL 报的错可能指向完全无关的行号,得倒着查。



















