ON CONFLICT 是 PostgreSQL 唯一可靠原子的 UPSERT 方式,必须基于唯一约束(UNIQUE/PRIMARY KEY),不可用普通索引或 NOT NULL;多约束需显式指定列名或约束名;DO UPDATE 中 WHERE 作用于原行,EXCLUDED 引用新值;RETURNING 需用 QueryRow/fetchone 获取更新后行。

ON CONFLICT 是 PostgreSQL 中唯一可靠、原子、可预测的 UPSERT 实现方式;其他“先查后插/更”的业务层逻辑在并发下必然出错,别试。
ON CONFLICT 必须基于唯一约束,否则直接报错
你写 ON CONFLICT (email),PostgreSQL 不会自动给你建索引。它只检查该列是否已有 UNIQUE 或 PRIMARY KEY 约束。没有就报这个错:
ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
常见误操作:
- 对着普通
INDEX(非唯一)用ON CONFLICT—— 不行 - 对着
NOT NULL列用 —— 不行 - 表刚建好,忘了加
UNIQUE(email)就跑 SQL —— 报错
验证方法:在 psql 里执行 \d 表名,确认目标列旁标着 UNIQUE 或 PK。
多唯一约束时,必须显式指定冲突目标
一张表有 email UNIQUE、phone UNIQUE、id PRIMARY KEY,你却只写 ON CONFLICT (id),那 email 冲突时照样报错,不会 fallback 到其他约束。
正确做法是二选一:
- 用列名:如果冲突只可能来自主键,就写
ON CONFLICT (id) - 用约束名:更安全,尤其当你需要响应
email冲突时,先查约束名:SELECT conname FROM pg_constraint WHERE conrelid = 'users'::regclass AND contype = 'u';,再写ON CONFLICT ON CONSTRAINT users_email_key - 联合唯一约束同理:比如
UNIQUE(user_id, product_id),就得写ON CONFLICT (user_id, product_id)
DO UPDATE 里用 WHERE 条件要格外小心
很多人加 WHERE products.status = 'active' 是为了“只更新有效商品”,但这个 WHERE 是作用于原表行(即冲突行)的,不是作用于新数据。一旦条件不成立,整个 DO UPDATE 就被跳过——既没插入,也没更新,语句还返回成功(INSERT 0 1),非常隐蔽。
容易踩的坑:
- 忘记加
WHERE导致误覆盖历史状态 - 加了
WHERE却没意识到它可能让整条 UPSERT “静默失效” - 在
DO UPDATE SET里引用EXCLUDED字段时,误写成VALUES()(那是 MySQL 的写法,PostgreSQL 不认)
正确写法示例:
INSERT INTO products (sku, price, status)
VALUES ('IPHONE_15', 7999, 'active')
ON CONFLICT (sku)
DO UPDATE SET price = EXCLUDED.price, updated_at = NOW()
WHERE products.status = 'active';
想拿到插入或更新后的 ID?RETURNING 必须配 QueryRow
很多人在 Go/Python 里用 db.Query(... RETURNING id),然后直接 rows.Scan(&id),结果 id 总是 0。这是因为 RETURNING 最多返回一行,而 db.Query() 要求先调 rows.Next() 才能读取。
真正安全的做法只有这一种:
- Go:必须用
db.QueryRow(),它内部自动处理单行逻辑,失败时返回sql.ErrNoRows - Python(psycopg2):用
cursor.fetchone(),别用cursor.fetchall() - psql 命令行:
RETURNING *没问题,但程序里不能假设多行
最常被忽略的一点:即使触发了 DO UPDATE,RETURNING 返回的仍是当前行(即更新后的那行)的值,不是原始插入值。这点在调试幂等写入逻辑时特别关键。

















