SQLite UPSERT 实战:更新已有记录时,别把 REPLACE 当成 UPDATE
导入库存时,“存在就更新,不存在就新增”很常见。把语句写成 INSERT OR REPLACE 看似省事,却可能让原有备注恢复成默认值。原因在于 REPLACE 的冲突处理与对原行执行 UPDATE 不同。下面用 Python 3.12.14 自带 sqlite3 接口演练,实际 SQLite 版本为 3.53.1;本文使用的单冲突目标 UPSERT 语法要求 SQLite 3.24.0 或更高。
AI生成概念图:记录的身份保持固定,仅替换指定的值。仅作概念说明,不代表实际界面或实测数据。
用一个会丢失的字段证明区别
样本只在内存中运行,包含固定编号、唯一商品编码、库存和备注。初始备注为 keep,而表默认值为 new。第一次 UPSERT 只更新库存,第二次 REPLACE 明确传回同一个编号却省略备注。这样不依赖自动编号行为,也能清楚观察两种语义。保存整段为 Python 文件即可运行。
import sqlite3
db = sqlite3.connect(":memory:")
try:
db.execute("""CREATE TABLE item(
id INTEGER PRIMARY KEY, sku TEXT NOT NULL UNIQUE,
stock INTEGER NOT NULL, note TEXT DEFAULT 'new'
)""")
db.execute("INSERT INTO item VALUES(7, 'tea', 3, 'keep')")
db.execute("""INSERT INTO item(sku, stock) VALUES(?, ?)
ON CONFLICT(sku) DO UPDATE SET stock = excluded.stock
""", ("tea", 8))
row = db.execute("SELECT id, sku, stock, note FROM item").fetchone()
assert row == (7, "tea", 8, "keep")
print("UPSERT:", row)
db.execute("""INSERT OR REPLACE INTO item(id, sku, stock)
VALUES(?, ?, ?)""", (7, "tea", 9))
row = db.execute("SELECT id, sku, stock, note FROM item").fetchone()
assert row == (7, "tea", 9, "new")
print("REPLACE:", row)
db.execute("""INSERT INTO item(sku, stock) VALUES(?, ?)
ON CONFLICT(sku) DO NOTHING""", ("tea", 100))
assert db.execute("SELECT stock FROM item").fetchone()[0] == 9
db.commit()
print("DO NOTHING: stock remains 9")
finally:
db.close()第一条输出保留编号七和备注 keep,库存变成八;第二条输出仍是编号七,但备注成为 new。相同主键数值并不能证明数据库保留了原来的行。此次实验专门用插入语句中省略的备注列作为探针,比只检查库存有没有变化更容易抓住问题。
先指定发生冲突的业务键
ON CONFLICT 后的 sku 对应真实的唯一约束。发生该约束冲突时,DO UPDATE 操作匹配的旧行;excluded.stock 表示本次原本想插入的新库存。若想做增量累加,可设计为旧库存加上新值,但必须先决定输入代表绝对库存还是变化量,二者在重复导入时结果完全不同。
不要把 excluded 理解成另一次查询返回的表,也不要遗漏冲突目标后就以为所有错误都被处理了。UPSERT 针对唯一性冲突,不能修复空值限制、检查约束或外键错误。更新动作若又违反约束,这条语句仍可能失败,调用方需要处理异常并安排事务边界。
REPLACE 会影响你没有写出的列
在唯一键或主键冲突时,REPLACE 会移除冲突行,再继续插入新行。新插入记录的省略列按默认值或相应规则取得值,而不是自动继承旧值。真实表若还有创建时间、审核状态或其他关联,这个区别会更明显;触发器和外键动作也应结合实际表结构专门验证。
因此迁移已有导入脚本时,先列出哪些列允许更新、哪些列必须保留,再写明确的 SET 清单。不要为了让语句跑通就把所有输入列覆盖回去,也不要直接假设 REPLACE 与 UPSERT 在日志、关联记录和触发器上等价。本文刻意没有引入这些对象,以便把备注变化这一证据看清楚。
把重复输入的行为写进验收
示例末尾的 DO NOTHING 表示发生指定冲突时不修改旧行,库存仍为九。它适合某些“已存在就跳过”的导入,但跳过不等于两条记录内容相同。如果输入库存是一百而数据库仍为九,业务上可能需要报告差异,而不是静默宣布导入全部成功。
生产验收至少准备新增键、已有键、缺少必填字段、违反其他约束四类输入,并核对返回结果和整批事务状态。多条语句构成一个业务动作时,应明确提交与回滚策略。最后检查保留字段,而不只看受影响行数;这才知道“更新成功”是否符合真正的数据规则。


