直接捕获唯一约束异常比先查再插更可靠,因后者存在竞态窗口、锁表风险和死锁隐患;生产环境应信任数据库约束,用ON DUPLICATE KEY UPDATE(MySQL)或ON CONFLICT DO UPDATE(PostgreSQL)实现幂等写入。

直接捕获唯一约束异常比先查再插更可靠,因为“先查再插”存在竞态窗口、锁表风险和死锁隐患;生产环境应信任数据库的约束检查能力,用 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或 INSERT ... ON CONFLICT DO UPDATE(PostgreSQL)实现幂等写入。
MySQL 用 ON DUPLICATE KEY UPDATE 更新而非跳过
该语法要求目标字段上有 PRIMARY KEY 或 UNIQUE 索引,冲突时更新指定字段,而不是静默丢弃。
-
VALUES(column)引用的是本次INSERT中该列的原始值,不是当前行旧值 - 避免用
REPLACE INTO:它本质是DELETE + INSERT,会丢失自增 ID、触发DELETE钩子、破坏外键引用计数 -
INSERT IGNORE仅静默跳过,无法更新,且mysql_affected_rows()返回 0,无法区分“真没插入”和“因冲突被忽略” - 示例:
INSERT INTO tb_user (id_card, username) VALUES ('370000199901016666','张三新名') ON DUPLICATE KEY UPDATE username = VALUES(username), updated_at = NOW();
PostgreSQL 必须显式指定冲突目标才能用 ON CONFLICT
PostgreSQL 不支持标准 SQL 的 MERGE,唯一合规的 UPSERT 方式是 ON CONFLICT,它必须基于唯一约束并显式指定冲突目标(索引名或列名),否则报错。
- 如果唯一约束是联合索引(如
(date, type)),必须写全列或明确索引名,不能只写ON CONFLICT (date) -
EXCLUDED是个伪表,代表本次想插入但被拒绝的那行数据,DO UPDATE SET username = EXCLUDED.username才能取到新值 - 加
WHERE条件可实现精细逻辑,例如“仅当旧值为空时才覆盖”:DO UPDATE SET username = EXCLUDED.username WHERE tb_user.username IS NULL - 示例:
INSERT INTO tb_user (id_card, username) VALUES ('370000199901016666','李四') ON CONFLICT ON CONSTRAINT un_id_card DO UPDATE SET username = EXCLUDED.username, updated_at = NOW();
报 UNIQUE constraint failed 时先查清冲突字段和值
错误本身不告诉你哪一行、哪个字段、哪个值撞了,得自己挖。直接看报错信息里的表名和约束名(比如 sqlite_error: UNIQUE constraint failed: users.email),然后立刻查这张表里已有的该字段值。
- 执行:
SELECT email,COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1; - 如果没查出重复,说明冲突来自你正插入的那条数据——但可能和已有数据“逻辑相等却字面不同”,比如空格、大小写、全角/半角、不可见字符(
\u200b、\t、\r\n) - 用
TRIM+LOWER+REPLACE检查字符串类唯一字段:SELECT LOWER(TRIM(REPLACE(REPLACE(email, CHAR(160),''),'\t',''))) AS clean_email,COUNT(*) FROM users GROUP BY clean_email HAVING COUNT(*) > 1; -
CHAR(160)是非断空格(常见于网页粘贴),TRIM()只清首尾空格,不清中间或制表符,所以额外REPLACE
Java 应用中精准捕获 SQLIntegrityConstraintViolationException
这个异常本身不直接暴露冲突字段名,需结合 SQLState 和错误消息定位。
- MySQL 报错码通常是
1062,SQLState为23000 - PostgreSQL 明确提示
DETAIL: Key (username)=(john) already exists.,可直接提取字段和值 - 在 JDBC URL 中添加
?loggerLevel=DEBUG(pgjdbc)或启用驱动日志,有助于还原完整上下文 - 不要只
catch SQLException,应专门捕获SQLIntegrityConstraintViolationException
真正难的不是语法,而是理解“冲突判定时机”和“约束覆盖范围”。例如,唯一索引允许 NULL,但 NULL != NULL,多行 NULL 并不违反唯一约束——这点常被忽略。

















