核心是用WHERE条件做原子性校验,而非SELECT+UPDATE;UPDATE必须含业务状态判断(如WHERE id=123 AND status='confirmed'),INSERT优先用ON DUPLICATE KEY UPDATE或MERGE,DDL需前置元数据检查。

用 WHERE 条件做原子性校验,别写 SELECT + UPDATE
存储过程里实现幂等,核心就一条:所有 UPDATE 或 INSERT 必须自带业务状态判断条件,靠数据库引擎的原子性兜底。比如更新订单状态,不能只写 UPDATE order SET status = 'shipped' WHERE id = 123,而要加状态前置检查:UPDATE order SET status = 'shipped', shipped_at = NOW() WHERE id = 123 AND status = 'confirmed'。这样哪怕脚本被调度系统重复触发十次,也只生效一次。
常见错误是先 SELECT status FROM order WHERE id = 123,应用层判断后再发 UPDATE。这在并发场景下必然失效——两个请求同时读到 'confirmed',都会执行更新。更糟的是,有些存储过程还显式加 BEGIN TRAN,反而把锁范围扩大、拖慢整体吞吐。
INSERT 场景优先用 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或 MERGE(PostgreSQL/Oracle)
对需要“有则更新、无则插入”的逻辑,硬写 IF EXISTS (...) THEN UPDATE ELSE INSERT 不仅啰嗦,还容易因竞态漏掉数据。MySQL 下直接用 INSERT INTO user_score (user_id, score) VALUES (1001, 100) ON DUPLICATE KEY UPDATE score = VALUES(score);PostgreSQL/Oracle 则用 MERGE INTO ... WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ...。这些语法由数据库原生保证原子性,无需手动查、判、插三步走。
注意点:
-
ON DUPLICATE KEY UPDATE依赖唯一索引或主键冲突触发,没建好索引会静默变成普通INSERT -
MERGE的ON条件必须能精确匹配目标行,否则可能误更新或漏插入 - 金仓KES、达梦等国产库对
MERGE支持程度不一,上线前务必在目标环境实测
DDL 变更必须前置存在性检查,别靠 TRY CATCH 捕获“已存在”异常
ALTER TABLE 脚本重复执行,在 MySQL 里会报 ERROR 1060 (42S21): Duplicate column name,但 PostgreSQL 报错信息不同,SQL Server 又是另一套。靠捕获异常来跳过,等于把兼容性问题交给运行时,极难维护。
正确做法是主动查元数据:
- MySQL:
SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'db_name' AND TABLE_NAME = 't1' AND COLUMN_NAME = 'new_col',结果为 0 再执行ADD COLUMN - PostgreSQL:
SELECT COUNT(*) FROM pg_attribute a JOIN pg_class c ON a.attrelid = c.oid WHERE c.relname = 't1' AND a.attname = 'new_col' - Oracle:
SELECT COUNT(*) FROM all_tab_columns WHERE table_name = 'T1' AND column_name = 'NEW_COL'
这类检查必须放在 DDL 语句之前,且整个块建议包装进存储过程,避免脚本散落在多个地方。
复杂状态流转建议用单字段枚举 + 状态机约束,别堆 if-else
当业务状态多于 3 种(如 'draft' → 'reviewing' → 'approved' → 'shipped' → 'cancelled'),在存储过程中用嵌套 IF status = 'draft' THEN ... ELSIF status = 'reviewing' THEN ... 容易漏路径、难测试。更稳的方式是定义一个 status 字段为 ENUM('draft','reviewing','approved','shipped','cancelled'),并在 WHERE 中明确限定合法跃迁:UPDATE order SET status = 'shipped' WHERE id = 123 AND status IN ('approved', 'reviewing')。
这样既防非法跳转(比如从 'draft' 直接到 'shipped'),又让幂等性自然成立——状态一旦进入 'shipped',后续所有含 IN ('approved','reviewing') 的更新都自动失效。
真正容易被忽略的是时间字段处理:NOW() 在 MySQL 中是语句开始时间,同一事务内多次调用值不变;但在 PostgreSQL 中可能随每次执行变化。若需记录“首次变更时间”,务必在应用层生成时间戳传入,别依赖数据库函数。

















