应优先使用merge(sql server)、insert...on duplicate key update(mysql)或insert...on conflict(postgresql),因其原子、高效、并发安全;而if exists+insert/update存在竞态窗口,易导致重复插入或漏更新。

直接用 MERGE(SQL Server)、INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或 INSERT ... ON CONFLICT(PostgreSQL),而不是在存储过程中写 IF EXISTS + INSERT/UPDATE。前者原子、高效、并发安全;后者窗口期大、易出错、性能差。
SQL Server:别手写 IF EXISTS,改用 MERGE + HOLDLOCK
手写 IF EXISTS(SELECT 1 FROM t WHERE id = @id) 再决定 INSERT 或 UPDATE,在并发下大概率触发唯一键冲突或漏更新——因为两次语句之间存在竞态窗口。
- 必须用
MERGE,且USING子句要基于主键或唯一索引列,否则报错或行为不可控 -
ON条件后加WITH (HOLDLOCK),例如FROM source_table WITH (HOLDLOCK),防止其他事务在判断后抢先插入 -
WHEN NOT MATCHED THEN INSERT必须显式列出所有非 NULL 列(含标识列若需显式插入,得先SET IDENTITY_INSERT ON) -
@@ROWCOUNT返回总影响行数,无法区分是 insert 还是 update;如需区分,得在OUTPUT子句中捕获 $action 或用临时表暂存状态
MySQL:ON DUPLICATE KEY UPDATE 是默认选项,但依赖唯一约束
只要表有主键或 UNIQUE 索引(比如 UNIQUE(email)),就该直接用 ON DUPLICATE KEY UPDATE。它不是“逻辑判断”,而是靠约束冲突触发的原子操作。
- 没建唯一约束就用这语法,会静默插入重复数据,或直接报错但不更新——先确认
SHOW CREATE TABLE users里有对应UNIQUE KEY -
UPDATE name = VALUES(name)中的VALUES(name)指本次INSERT子句里的值,不是变量名,别写成name = @name或name = name - 批量插入时,避免在
ON DUPLICATE KEY UPDATE中调用NOW()多次——每次都会重新求值,不是事务开始时间点 - 别用
REPLACE INTO:它本质是DELETE + INSERT,会跳变自增 ID、误触外键级联、重复执行触发器
PostgreSQL:ON CONFLICT 要显式指定冲突目标,EXCLUDED 不是 NEW
PostgreSQL 不支持 MySQL 那种隐式冲突检测,ON CONFLICT 后必须跟具体列(或索引名),且这些列必须有唯一索引支撑。
-
ON CONFLICT (email)要求CREATE UNIQUE INDEX ON users(email)已存在;联合索引则写ON CONFLICT (tenant_id, code),顺序必须一致 - 更新时用
EXCLUDED.name,不是NEW.name(NEW只在触发器中有效);EXCLUDED是关键字,代表本次想插入但被挡下的那行数据 - 如果只想“存在就跳过”,用
ON CONFLICT DO NOTHING,但此时ROW_COUNT()返回 0,无法区分是跳过还是真插入了——需额外查或用RETURNING捕获结果 - 仅靠
EXCEPTION WHEN unique_violation捕获冲突再UPDATE是退化方案,高并发下仍可能因竞态失败,应优先用ON CONFLICT
所有数据库都必须避开的硬伤:循环 + 单行 INSERT
在存储过程中写 WHILE @i ,等于主动放弃数据库的批量能力。
- 每轮循环都是独立语句解析+IO+锁申请,1000 行可能比一条批量
INSERT慢 50 倍以上 - 没显式事务包裹时,中间失败就卡在半途,部分数据已提交,部分未执行,一致性全靠运气
- 真正需要分批(比如超 5000 行),应按主键范围切片:
WHERE id BETWEEN @start AND @end,而非计数器 - MySQL 中
LIMIT在存储过程里不能用于UPDATE语句;SQL Server 的SET ROWCOUNT已被弃用,别用
最常被忽略的一点:无论用哪种 upsert 语法,都必须包裹在显式事务中,并检查错误码。原子性只保证单条语句不中断,不保证业务语义完整——比如插入成功但后续更新余额失败,得靠 TRY...CATCH(SQL Server)或 DECLARE EXIT HANDLER(MySQL)兜底。











