作为一名长期与MySQL打交道的开发者,我经常遇到需要"存在即更新,不存在则插入"的场景。这种需求在用户数据同步、统计计数、配置管理等业务中尤为常见。MySQL提供的ON DUPLICATE KEY UPDATE语法完美解决了这个问题,但其中有不少细节和坑需要注意。
ON DUPLICATE KEY UPDATE是MySQL对标准SQL的扩展语法,它的核心逻辑可以分为三个步骤:
这个过程中有几个关键技术细节值得注意:
提示:在8.0.19版本后,MySQL还支持VALUES()函数的替代语法,使用
AS alias形式,如UPDATE age = new_values.age,这在复杂查询中可读性更好。
很多开发者会混淆ON DUPLICATE KEY UPDATE和REPLACE INTO,两者虽然都能实现"存在即更新"的效果,但有本质区别:
| 特性 | ON DUPLICATE KEY UPDATE | REPLACE INTO |
|---|---|---|
| 执行逻辑 | 先尝试INSERT,冲突时UPDATE | 先DELETE再INSERT |
| 受影响行数 | 1(插入)或2(更新) | 1(插入)或大于1(删除后插入) |
| 自增ID | 只增加不重用 | 可能重用已删除的ID |
| 触发器 | 只触发INSERT或UPDATE | 触发DELETE和INSERT |
| 性能 | 更高 | 较低 |
实际开发中,除非确实需要先删除再插入的逻辑,否则更推荐使用ON DUPLICATE KEY UPDATE。
让我们通过一个用户表示例来演示基本用法。首先创建测试表:
sql复制CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
login_count INT DEFAULT 0,
last_login DATETIME,
profile_data JSON
) ENGINE=InnoDB;
sql复制INSERT INTO users (id, username, login_count, last_login)
VALUES (1, 'john_doe', 1, NOW())
ON DUPLICATE KEY UPDATE
login_count = login_count + 1,
last_login = NOW();
这个语句会在id=1存在时增加登录次数并更新最后登录时间,不存在时创建新用户。
sql复制INSERT INTO users (username, login_count, last_login)
VALUES ('jane_doe', 1, NOW())
ON DUPLICATE KEY UPDATE
login_count = login_count + VALUES(login_count),
last_login = VALUES(last_login);
这里使用了VALUES()函数引用INSERT部分的值,使SQL更加清晰。
ON DUPLICATE KEY UPDATE支持批量操作,这在数据同步场景非常有用:
sql复制INSERT INTO users (username, login_count, last_login)
VALUES
('user1', 1, NOW()),
('user2', 1, NOW()),
('user3', 1, NOW())
ON DUPLICATE KEY UPDATE
login_count = VALUES(login_count),
last_login = VALUES(last_login);
注意:批量操作时,所有行共享同一个UPDATE语句,无法为不同行设置不同的更新逻辑。
有时我们需要根据条件决定是否更新某些字段,这时可以结合CASE WHEN或IF函数:
sql复制INSERT INTO users (username, login_count, last_login, profile_data)
VALUES ('test_user', 1, NOW(), '{"premium": true}')
ON DUPLICATE KEY UPDATE
login_count = IF(VALUES(login_count) > 100, VALUES(login_count), login_count),
profile_data = CASE
WHEN profile_data->>'$.premium' = 'true' THEN profile_data
ELSE VALUES(profile_data)
END;
这个例子展示了:
在Java项目中使用MyBatis时,ON DUPLICATE KEY UPDATE有两种主要写法:
xml复制<insert id="upsertUser">
INSERT INTO users (username, login_count, last_login)
VALUES (#{username}, #{loginCount}, #{lastLogin})
ON DUPLICATE KEY UPDATE
login_count = VALUES(login_count),
last_login = VALUES(last_login)
</insert>
这种写法支持批量操作,且字段名自动映射。
xml复制<insert id="upsertUser">
INSERT INTO users (username, login_count, last_login)
VALUES (#{username}, #{loginCount}, #{lastLogin})
ON DUPLICATE KEY UPDATE
login_count = #{loginCount},
last_login = #{lastLogin}
</insert>
这种写法可以在更新时使用不同的逻辑,如:
xml复制ON DUPLICATE KEY UPDATE
login_count = login_count + #{increment},
last_login = #{lastLogin}
实测对比(10000条记录):
| 方式 | 耗时(ms) | 锁持有时间 |
|---|---|---|
| 单条INSERT+SELECT | 5200 | 长 |
| ON DUPLICATE KEY | 1200 | 短 |
| 批量ON DUPLICATE | 350 | 很短 |
这是ON DUPLICATE KEY UPDATE最常见的问题之一。现象是即使执行的是更新操作,自增ID也会递增。这是因为:
解决方案:
ON DUPLICATE KEY UPDATE确实可能引发死锁,典型场景是:
规避策略:
如文中提到的,唯一键的大小写敏感性会影响冲突判断:
sql复制-- 大小写不敏感的排序规则
CREATE TABLE case_insensitive (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci UNIQUE
);
-- 大小写敏感的排序规则
CREATE TABLE case_sensitive (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin UNIQUE
);
在case_insensitive表中,'John'和'JOHN'被视为冲突;而在case_sensitive表中则不会。
经过多年使用,我总结了以下ON DUPLICATE KEY UPDATE的最佳实践:
一个典型的生产环境示例:
sql复制-- 用户行为数据批量更新
INSERT INTO user_behavior (user_id, item_id, action_type, count, update_time)
VALUES
(1001, 2001, 'click', 1, NOW()),
(1001, 2002, 'view', 1, NOW()),
(1002, 2001, 'purchase', 1, NOW())
ON DUPLICATE KEY UPDATE
count = IF(action_type = VALUES(action_type), count + VALUES(count), VALUES(count)),
update_time = NOW();
这个例子实现了:
在实际项目中,ON DUPLICATE KEY UPDATE帮我简化了大量"先查询再判断插入或更新"的样板代码,特别是在处理实时统计、用户偏好设置等场景时。掌握它的各种细节和陷阱后,可以既保证数据一致性又获得良好的性能表现。