merge into在高并发下会因“检查-然后-执行”竞态导致唯一键冲突,且存在dml重启动缺陷;需加where子句防护,或改用insert+异常捕获,同时排查序列值偏小问题。

为什么MERGE INTO在高并发下会出错?
MERGE INTO本身不是原子操作,Oracle会在执行时先判断ON条件是否匹配,再决定走WHEN MATCHED THEN UPDATE还是WHEN NOT MATCHED THEN INSERT。但在高并发场景下,两个会话可能同时查到“不存在”,都尝试INSERT,最终触发唯一键冲突(比如ORA-00001: unique constraint violated)。这不是语法错误,而是典型的“检查-然后-执行”(check-then-act)竞态。
用WHERE子句补全ON条件能避免重启动异常
Oracle的MERGE INTO在某些版本(10g~23ai)存在DML重启动行为缺陷:当ON条件依赖的字段被其他事务修改后,UPDATE分支可能基于旧值执行,导致逻辑错乱(比如得到(10, 'Tom')这种理论不可能的结果)。官方明确建议在WHEN MATCHED THEN UPDATE后显式加上WHERE子句,把ON里的关键条件再写一遍:
MERGE INTO test_dml_restart t1 USING (SELECT * FROM DUAL) t2 ON (t1.id = 1) WHEN MATCHED THEN UPDATE SET t1.name = 'Tom' WHERE t1.id = 1 WHEN NOT MATCHED THEN INSERT (id, name) VALUES (1, 'Tom');
-
WHERE t1.id = 1不是冗余——它确保UPDATE只作用于当前行,防止重启动后误更新 - 这个
WHERE必须和ON中的谓词严格一致,否则起不到防护作用 - 对带精度数字字段(如
NUMBER(10,2)),还要注意ODBC绑定变量未做精度截断的问题,此时需在应用层主动round或转SQL_NUMERIC_STRUCT
真正可靠的方案是改用INSERT … SELECT + 异常捕获
当业务允许“插入失败就更新”语义时,比MERGE INTO更稳的方式是分两步:先尝试INSERT,捕获ORA-00001后执行UPDATE。Oracle 21c支持INSERT ... SELECT ... WHERE NOT EXISTS,但不如异常处理直接:
BEGIN
INSERT INTO test_dml_restart (id, name) VALUES (1, 'Tom');
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
UPDATE test_dml_restart SET name = 'Tom' WHERE id = 1;
END;
- PL/SQL块天然串行化,不会出现两个会话同时走到UPDATE分支
- 比
MERGE更容易调试——你可以单独跑INSERT看是否真冲突,而不是怀疑ON逻辑 - 如果表有多个唯一约束(比如
id和code都唯一),MERGE的ON只能选一个字段,而异常捕获可统一处理所有冲突
序列值偏小也会引发主键冲突,别只盯着SQL
高并发插入失败有时根本不是SQL逻辑问题,而是序列SEQ_TEST.NEXTVAL返回的值小于表中已有最大主键。比如导数后没重置序列,下次INSERT就可能撞上已存在的id。
- 查当前序列值:
SELECT SEQ_TEST.CURRVAL FROM DUAL(需先调一次NEXTVAL) - 查表最大主键:
SELECT MAX(id) FROM test_dml_restart - 安全重置序列:
ALTER SEQUENCE SEQ_TEST INCREMENT BY 100 START WITH [max_id+100] MINVALUE 1;,再执行一次NEXTVAL,最后还原增量 - 金仓等Oracle兼容库也支持此操作,但要注意
CURRVAL在部分国产库中行为略有差异
真正容易被忽略的是:序列偏小 + 高并发 + MERGE,三者叠加会让错误看起来像并发逻辑缺陷,实际根源在元数据层面。先确认序列值,再优化SQL,顺序不能反。











