INSERT OR REPLACE 本质是 DELETE + INSERT,非原子更新,会触发外键级联、自增ID跳号及触发器重复执行;应优先使用 ON CONFLICT DO UPDATE 实现真正“存在则更新”。

INSERT OR REPLACE 会触发 DELETE + INSERT,不是 UPDATE
这是最根本的原因:SQLite 的 INSERT OR REPLACE 并不真正“更新”行,而是先按冲突的 UNIQUE 或 PRIMARY KEY 找到旧记录,整行删除,再插入新行。如果表有外键关联(比如 ON DELETE CASCADE / SET NULL),这个 DELETE 就会激活级联行为,导致关联数据被删或置空。
- 例如:主表
orders有id PRIMARY KEY,从表order_items用order_id外键引用它,并设了ON DELETE CASCADE - 执行
INSERT OR REPLACE INTO orders VALUES (123, ...)时,SQLite 先DELETE FROM orders WHERE id = 123 - 这个 DELETE 触发级联,
order_items中所有order_id = 123的记录被自动删掉 - 然后才插入新
orders行 —— 但关联项已丢失,无法恢复
UNIQUE 约束字段选错也会“误删”
当 INSERT OR REPLACE 冲突目标不是你预期的字段,就可能删掉不该删的行。它只看约束定义,不看业务逻辑。
- 建表语句是
CREATE TABLE users (id INTEGER, email TEXT, UNIQUE(email)),没设主键 - 执行
INSERT OR REPLACE INTO users (id, email) VALUES (100, 'a@b.com'),冲突发生在email上 - SQLite 删除所有
email = 'a@b.com'的行(哪怕有多个不同id),再插入新行 - 结果:同邮箱的旧用户全被干掉,只剩一条 —— 这不是“替换”,是“覆盖+清场”
替代方案:优先用 UPSERT(ON CONFLICT)
SQLite 3.24.0+ 支持标准 ON CONFLICT 语法,能真正做原子级“存在则更新”,不触发 DELETE,也就不会波及外键。
- 正确写法:
INSERT INTO users (id, name, email) VALUES (100, 'Alice', 'alice@x.com') ON CONFLICT(id) DO UPDATE SET name=excluded.name, email=excluded.email -
excluded是虚拟表,代表本次想插入的值;users.name指原表当前值,可用来做条件或累加 - 外键不受影响:没有 DELETE 动作,级联规则完全不触发
- 注意:
ON CONFLICT必须明确指定冲突目标(如ON CONFLICT(id)或ON CONFLICT(email)),不能省略
真要用 INSERT OR REPLACE,必须确认无外键依赖
如果你的表确实没定义任何外键,或者外键没设 ON DELETE 行为(即默认 NO ACTION),那 INSERT OR REPLACE 才相对安全。
- 检查外键:运行
PRAGMA foreign_key_list(table_name),看是否有ON DELETE非空值 - 检查约束:用
PRAGMA index_list(table_name)和PRAGMA index_info(index_name)确认 UNIQUE/PRIMARY KEY 覆盖哪些列 - 测试时别跳过事务封装:即使没外键,也建议用
BEGIN IMMEDIATE包裹批量INSERT OR REPLACE,避免中间状态被读取
真正容易被忽略的是:很多人以为 REPLACE 是“安全更新”,但只要表结构里藏了外键或非主键 UNIQUE 约束,它就在悄悄删数据。别凭直觉判断,查 PRAGMA 结果说话。










