SQLite 3.24.0+ 原生支持 UPSERT,但必须满足两个硬性前提:目标列需有 UNIQUE 或 PRIMARY KEY 约束,且 ON CONFLICT 后必须明确指定括号内的冲突目标(如 ON CONFLICT (id)),不可省略括号或使用 PostgreSQL 风格的 ON CONSTRAINT;更新时须用小写 excluded.col 引用新值,不支持 VALUES(col) 或大写 EXCLUDED;ON CONFLICT 不触发更新的前提是约束实际存在,否则语句静默失效。

SQLite 3.24.0+ 支持原生 INSERT ... ON CONFLICT ... DO UPDATE,但必须满足两个硬性前提:目标列有 UNIQUE 或 PRIMARY KEY 约束,且语法中 ON CONFLICT 后的括号不能省略(哪怕只写一个字段)。
ON CONFLICT 后必须指定冲突目标列或约束名
SQLite 不接受模糊写法,比如 ON CONFLICT DO UPDATE 会直接报错:ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification。它需要你明确告诉它“和谁冲突”。
- 主键冲突:写
ON CONFLICT (id)(假设id是主键或带UNIQUE的列) - 复合唯一索引冲突:写
ON CONFLICT (tenant_id, external_id) - 不能写
ON CONFLICT id(漏括号)、ON CONFLICT ON CONSTRAINT xxx(SQLite 不支持该 PostgreSQL 写法) - 表结构没建唯一约束?先加
CREATE UNIQUE INDEX idx_email ON users(email);,否则 UPSERT 不生效
更新时用 excluded.col 引用新值,不是 VALUES(col)
MySQL 的 VALUES(col) 在 SQLite 里无效;PostgreSQL 的 EXCLUDED.col(大写)也不行——SQLite 只认小写的 excluded.col。这个虚拟表代表本次 INSERT 尝试写入但被挡下的那行数据。
- 正确:
name = excluded.name、login_count = users.login_count + excluded.login_count - 错误:
name = VALUES(name)(语法错误)、name = EXCLUDED.name(大小写敏感,报错) - 想保留原值当新值为空?SQLite 不支持
DO UPDATE SET ... WHERE excluded.name IS NOT NULL—— 它不支持WHERE子句,得用CASE:name = CASE WHEN excluded.name IS NULL THEN users.name ELSE excluded.name END
INSERT OR REPLACE 看似简单,但本质是删再插
它不是原子更新,而是一次 DELETE 加一次 INSERT,在有外键、触发器或自增主键连续性要求的场景下极易出问题。
- 会触发
BEFORE DELETE和AFTER INSERT触发器,审计日志多记两笔 - 若子表有
ON DELETE CASCADE,关联记录会被连带删除 -
id INTEGER PRIMARY KEY AUTOINCREMENT会跳号(删掉 id=5 后再 replace,新 id 可能是 6 而非 5) - 想避免这些副作用?必须用
ON CONFLICT ... DO UPDATE,而不是REPLACE INTO
最容易被忽略的是:SQLite 的 ON CONFLICT 语法不检查字段是否真有唯一性保障——它只按你写的列名去查约束。如果表结构没建索引,语句能执行但永远不触发更新。上线前务必用 PRAGMA table_info(users); 和 PRAGMA index_list(users); 验证约束真实存在。

















