直接结论:必须用唯一约束兜底,配合MERGE或异常捕获ORA-00001;仅靠SELECT WHERE NOT EXISTS无法避免高并发重复,因查询不加锁,两个会话可同时判定“不存在”后插入。

直接结论:用 MERGE 替代 INSERT,或在 INSERT 前加 SELECT ... WHERE NOT EXISTS 子查询;但必须配合唯一约束(UNIQUE 或 PRIMARY KEY)兜底,否则并发场景下仍会重复。
为什么 INSERT ... SELECT WHERE NOT EXISTS 在高并发时会失效
表面看逻辑严密:先查再插,但 Oracle 中两个会话可能在同一时刻都查到“不存在”,然后都执行插入——因为 SELECT 不加锁(除非显式 FOR UPDATE,但那会阻塞、降低吞吐)。这不是 bug,是事务隔离级别的自然表现。
实操建议:
- 不要依赖纯应用层判断做“防重”,必须有数据库级强制约束
- 若业务允许,把关键字段组合设为
UNIQUE约束,让 Oracle 自动报ORA-00001错误 - 捕获
ORA-00001后忽略或记录日志,比预防性查表更可靠
用 MERGE 实现“有则更新、无则插入”的原子操作
MERGE 是 Oracle 原生支持的 upsert 语句,整个匹配 + 插入/更新过程在单条语句内完成,天然避免竞态。适用于需要幂等写入的场景(如定时同步、接口幂等落库)。
示例(向 user_log 表插入日志,按 user_id 和 log_date 去重):
MERGE INTO user_log t USING (SELECT :p_user_id AS user_id, :p_log_date AS log_date, :p_content AS content FROM DUAL) s ON (t.user_id = s.user_id AND t.log_date = s.log_date) WHEN NOT MATCHED THEN INSERT (user_id, log_date, content) VALUES (s.user_id, s.log_date, s.content);
注意点:
-
ON条件列必须有索引(最好是唯一索引),否则性能急剧下降 - 不写
WHEN MATCHED THEN UPDATE就是纯“防重插入”,写了才是完整 upsert - 绑定变量(如
:p_user_id)比拼接字符串更安全,也利于 SQL 共享池复用
存储过程中如何安全地捕获重复错误并跳过
当已有唯一约束且不想改逻辑时,可在存储过程中用异常处理屏蔽 ORA-00001,让流程继续。
示例片段:
BEGIN
INSERT INTO user_config (user_id, config_key, config_value)
VALUES (p_user_id, p_key, p_value);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
NULL; -- 忽略重复,不报错
WHEN OTHERS THEN
RAISE; -- 其他错误原样抛出
END;关键提醒:
-
DUP_VAL_ON_INDEX是预定义异常,只捕获唯一/主键冲突,比SQLCODE = -1更精准 - 不要在异常块里写复杂逻辑(比如再查一遍再插),那又回到竞态问题
- 如果业务要求“插入成功才发消息”,需把发消息逻辑放在
INSERT后、EXCEPTION前,否则重复时也会触发
容易被忽略的隐性重复来源:序列 + 手动拼接主键
有些老代码用 SEQ.NEXTVAL 拼出字符串主键(如 'U' || SEQ.NEXTVAL),再 INSERT。这看似唯一,但如果存储过程被多次调用且未提交,序列值已递增,但事务回滚后该值不会回收——下次调用仍会生成新值,导致“逻辑重复”(业务上同一数据被插了多份,只是主键不同)。
对策:
- 主键应由业务语义决定(如
user_id + event_type),而非靠序列“强行唯一” - 若必须用序列,确保去重依据不依赖序列值,而是真实业务字段
- 避免在循环中反复调用
NEXTVAL后又回滚,可先缓存序列值再批量处理
真正可靠的防重,永远是“约束 + 原子语句 + 明确的异常边界”。靠条件判断、时间戳、或应用层标记,都会在并发或重试场景下漏掉。Oracle 的 MERGE 和唯一约束不是备选方案,是必选项。


















