在PostgreSQL里,当我们需要往一张带有主键或唯一索引的表中写入数据,而数据源里可能包含已经存在的记录时,如果直接执行普通的INSERT语句,数据库就会抛出唯一约束冲突的错误。为了避免这种情况,早期的做法往往是先执行一次SELECT查询确认记录是否存在,不存在再插入,或者捕获SQL异常后忽略。这类方式不仅增加了与数据库的交互次数,在并发场景下还容易出现竞态条件。PostgreSQL从9.5版本开始引入了INSERT ON CONFLICT子句,其中的DO NOTHING选项可以让数据库在碰到冲突行时自动跳过,而不中断整批写入,是实现高效冲突插入处理的推荐方案。

一、INSERT ON CONFLICT DO NOTHING基本语法
该语法的核心是在INSERT语句后面追加ON CONFLICT冲突目标判定以及对应的处理动作。冲突目标通常指定为某个唯一约束、主键或者唯一索引列,当插入行与已有行在这些列上取值重复时,数据库便触发冲突处理逻辑。DO NOTHING表示直接忽略当前这一行,继续处理后续行。
下面给出一个最基础的示例,假设我们有一张用户表,user_id是主键:
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
user_name TEXT,
create_time TIMESTAMP DEFAULT NOW()
);
-- 尝试插入,如果user_id冲突则什么也不做
INSERT INTO users (user_id, user_name)
VALUES (1001, '张三')
ON CONFLICT (user_id)
DO NOTHING;
上述语句第一次执行会正常插入,第二次以相同user_id执行时,PostgreSQL检测到主键冲突,便静默跳过,不会报错也不会改变原记录。这种方式比先查后插更简洁,也避免了应用层写复杂的判断逻辑。
二、为什么它比先查询再插入更高效
从执行原理上看,ON CONFLICT DO NOTHING是在同一个原子语句内部借助唯一索引的探测来完成判重。数据库在写入阶段就能发现重复键,并立即放弃该元组插入,不需要额外发起网络往返。而先SELECT再INSERT至少需要两次客户端与服务端交互,且在两次操作之间,其他事务可能插入相同记录,导致后者插入依然失败。
我们可以通过一个简单的对比来理解开销差异。以下代码展示了应用层常见的先查后插写法:
import psycopg2
conn = psycopg2.connect("dbname=test user=postgres")
cur = conn.cursor()
def insert_if_not_exists(uid, name):
cur.execute("SELECT 1 FROM users WHERE user_id = %s", (uid,))
if cur.fetchone() is None:
cur.execute("INSERT INTO users (user_id, user_name) VALUES (%s, %s)", (uid, name))
conn.commit()
insert_if_not_exists(1001, '张三')
这种写法在每次插入前都要查询一次,批量导入十万条数据时就是十万次额外查询。如果改用单条SQL的ON CONFLICT DO NOTHING,或者使用批量插入配合该子句,就能把判重工作完全下推到数据库引擎,大幅减少开销。此外,在可重复读及以上隔离级别中,先查后插还可能因并发写入而出现序列化失败,而ON CONFLICT由数据库统一加锁处理,更加安全。
三、批量插入中的实际应用
在数据同步、日志去重等场景中,我们往往要一次性写入多行。PostgreSQL支持在VALUES后列举多行,并统一附加ON CONFLICT DO NOTHING,这样整批数据中只有冲突行被忽略,其余正常入库。
示例如下:
INSERT INTO users (user_id, user_name)
VALUES
(1001, '张三'),
(1002, '李四'),
(1003, '王五')
ON CONFLICT (user_id)
DO NOTHING;
如果1002已经存在,那么只有1001和1003被插入,1002那一行被跳过,语句整体执行成功。对于ETL任务来说,这种语义非常友好:我们无需在抽取层做复杂去重,只需保证目标表约束正确,让数据库自己处理重复。
需要注意的是,若表上有多个唯一约束,可以通过指定冲突目标来精确控制。例如ON CONFLICT (email) DO NOTHING只忽略email冲突,而user_id冲突仍会报错。如果要忽略任意唯一约束冲突,可使用ON CONFLICT DO NOTHING而不写具体目标,但要求表上至少有一个唯一约束。
四、DO NOTHING与DO UPDATE的区别
很多初学者容易混淆ON CONFLICT的两种处理动作。DO NOTHING仅仅是跳过,原记录保持不动;而DO UPDATE可以在冲突时更新已有记录的部分字段,也就是俗称的upsert。
对比示例:
-- 冲突时跳过 INSERT INTO users (user_id, user_name) VALUES (1001, '新名字') ON CONFLICT (user_id) DO NOTHING; -- 冲突时更新名字 INSERT INTO users (user_id, user_name) VALUES (1001, '新名字') ON CONFLICT (user_id) DO UPDATE SET user_name = EXCLUDED.user_name;
如果你的业务只关心“有这条数据就行,别报错”,那么DO NOTHING最合适;如果需要用最新数据覆盖旧数据,则必须选择DO UPDATE。错误使用DO NOTHING可能导致数据一直停留在旧状态,在排查问题时容易让人误以为写入成功却没生效。
五、使用时的注意事项与性能建议
首先,ON CONFLICT DO NOTHING依赖唯一索引或主键来判定冲突,如果表上没有相应约束,数据库无法识别哪些算冲突,语句虽能执行但起不到忽略作用。因此设计表结构时应提前规划好唯一性约束。
其次,在极大批量写入时,建议结合COPY或者分批事务提交,而不是在单个事务里堆入百万级INSERT。虽然ON CONFLICT本身高效,但事务过长会占用锁和日志空间。可以参考以下分批逻辑:
batch = []
for item in data_source:
batch.append(item)
if len(batch) >= 1000:
cur.executemany(
"INSERT INTO users (user_id, user_name) VALUES (%s, %s) "
"ON CONFLICT (user_id) DO NOTHING",
batch
)
conn.commit()
batch.clear()
最后,如果在读写分离架构中,要注意ON CONFLICT只在主库执行,从库通过流复制同步结果。应用层不应假设插入被忽略后立刻能在从库查到最新状态,需考虑复制延迟。总体而言,合理使用INSERT ON CONFLICT DO NOTHING可以让PostgreSQL的冲突插入处理既安全又高效。
PostgreSQLINSERT_ON_CONFLICT冲突插入修改时间:2026-08-11 03:36:33