报 unique constraint failed 时需先查冲突字段值,再用 trim、lower、replace 清洗检查隐性重复(如空格、大小写、全角/半角、不可见字符),并借助 insert ... where not exists 或 on conflict 定位问题,而非依赖应用层预检。

INSERT 报 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 检查字符串类唯一字段
邮箱、用户名、手机号这类字段最容易因格式差异触发隐性重复。别只靠肉眼比对,用 SQL 做标准化清洗后查重:
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 - SQLite 不支持
REGEXP,如需去所有空白符,可用嵌套REPLACE或改用TRANSLATE(PostgreSQL)或正则函数(MySQL 8.0+)
INSERT OR IGNORE / ON CONFLICT 不是兜底方案,而是暴露问题的开关
加 INSERT OR IGNORE(SQLite)或 ON CONFLICT DO NOTHING(PostgreSQL)后不报错,不代表数据没问题——它只是静默丢弃冲突行,你可能根本不知道哪条丢了。
- 想定位问题:先用
INSERT ... SELECT ... WHERE NOT EXISTS包裹,把“会被忽略”的记录单独捞出来 - 想保留新数据:用
ON CONFLICT (email) DO UPDATE SET updated_at = excluded.updated_at,但必须确认业务允许覆盖 - MySQL 用
INSERT IGNORE或ON DUPLICATE KEY UPDATE,注意后者会触发AUTO_INCREMENT值跳增
应用层写入前做唯一性预检,但要注意竞态窗口
在代码里先 SELECT COUNT(*) WHERE email = ? 再 INSERT,看似稳妥,其实存在微小时间窗口:两个请求几乎同时通过检查,然后都插入成功——数据库层面仍会报唯一冲突。
- 真正安全的做法:依赖数据库约束本身,把
UNIQUE当作事实校验器,而非靠应用层规避 - 捕获异常后,再用标准化查询(如上文
LOWER(TRIM(...)))反查冲突源,日志里记下原始值和清洗后值,方便溯源 - 前端提交前可做轻量清洗(如
trim().toLowerCase()),但不能替代后端校验——用户可能绕过前端
最麻烦的不是重复,而是“看起来不重复”的重复。空格、编码、大小写、不可见字符,这些不会报错,但会让唯一约束形同虚设。










