资讯动态

PostgreSQL UPSERT 详解:INSERT ON CONFLICT 语法、场景与避坑指南

发布时间:2026/10/8 9:18:55 来源:尧图企业网站定制
1. 为什么说 UPSERT 是写入逻辑里最容易踩竞态的“安全绳”做了这么多年后端我最怕看到的一种代码模式就是业务里为了保证“有就更新、没有就插入”先写一条SELECT判断记录是否存在再决定走INSERT还是UPDATE。这种写法在并发量上来之后几乎必然出事。两个请求同时查到“不存在”然后同时执行插入后到的那条直接撞上唯一约束报错或者两个请求同时查到“已存在”同时执行更新后到的覆盖了先到的中间状态。明明加了唯一索引结果错误还是漏到应用层了。PostgreSQL 的 UPSERT也就是INSERT ... ON CONFLICT解决的就是这个问题。它把“尝试插入遇到冲突就改走更新或忽略”这一个完整决策交给了数据库本身的并发机制去处理。你不用再自己写查询判断不用锁表也不用在应用层做分布式锁一句话就能把竞态窗口关掉。这也是为什么从 PostgreSQL 9.5 加入这个特性之后它迅速成了后端写入逻辑的首选方案。这篇内容我打算从一个实际使用者的角度把INSERT ON CONFLICT的语法细节、执行机制、真实场景、常见坑以及和 MySQL 那张著名的ON DUPLICATE KEY UPDATE的区别一次性讲透。主要面向三类人刚接触 PostgreSQL 想写对 upsert 的开发者、从 MySQL 迁移过来需要对照学习的后端以及需要排查线上“重复插入”或“死锁”问题的 DBA。文章里的 SQL 我都基于 PostgreSQL 14 以上的版本验证过但核心语法在 9.5 之后的版本都能跑。先说结论INSERT ON CONFLICT不是你写不写的问题而是你能否把它的细节用对的问题。语法本身非常简单但背后涉及唯一约束、部分索引、锁与事务可见性、序列消耗这些容易被忽略的行为。一次写不对轻则 SQL 报错重则并发下死锁反过来把整个业务拖垮。2. INSERT ON CONFLICT 语法拆解从“静默忽略”到“精确更新”2.1 最简单的分支ON CONFLICT DO NOTHING先看基础语法INSERT INTO users (email, nickname) VALUES (xiaomingexample.com, 小明) ON CONFLICT (email) DO NOTHING;这行 SQL 的行为是如果能正常插入就插入如果插入时违反了email上的唯一约束则什么都不做也不再报错。注意这里的ON CONFLICT (email)不是随便写的它必须对应一个实际存在的唯一索引或唯一约束的列。如果你写了一个没有唯一索引的列名PostgreSQL 会直接报错ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification很多初学者在这里卡半天明明字段存在为什么说没有匹配的约束因为ON CONFLICT是靠“索引”来判定冲突的不是靠普通字段。你可以在users表上建立CREATE UNIQUE INDEX ON users (email)或者在建表时写email TEXT UNIQUE它才会作为合法的冲突目标。没有索引数据库根本没有能力判断“冲突”这件事和你手动SELECT之前必须有个主键是一个道理。DO NOTHING适合什么场景最典型的是注册账号时用邮箱或手机号做唯一标识。用户重复点击注册按钮第二次请求进来时直接静默忽略不需要覆盖已有记录也不需要报“已注册”错误给用户。应用层只需要检查INSERT返回的受影响行数返回 1 表示新注册成功返回 0 表示这条记录早已存在。我提醒一点DO NOTHING并不是完全不消耗资源的免费操作。它在执行时仍然要尝试插入、检查唯一索引、发现冲突然后丢弃写入意图。在高并发下如果大量冲突发生唯一索引的检查与锁等待依然会产生性能开销。所以不要把它当作“便宜的幂等方案”它是“正确但不一定便宜”的方案。2.2 核心更新分支DO UPDATE 与 EXCLUDED 伪表真正让 UPSERT 有名字的是DO UPDATE分支INSERT INTO user_points (user_id, points) VALUES (1001, 50) ON CONFLICT (user_id) DO UPDATE SET points user_points.points EXCLUDED.points, updated_at now();这里的EXCLUDED是 PostgreSQL 里一张特殊的伪表里面装着“本来打算插入的那一行数据”。所以EXCLUDED.points就是这次想加进去的 50 分user_points.points是当前已在表里的旧值。两者相加就实现了积分累加。这个写法很值得多说几句因为它彻底解决了“插入时不知道冲突后的旧值是什么”的问题。假设没有EXCLUDED你要实现同样的逻辑只能先SELECT出旧积分算好新值后再UPDATE。这又回到了最开始说的竞态问题两个请求同时读到旧积分各自算完新值后写回后写的覆盖先写的积分直接少算一次。而EXCLUDED是在数据库决定走DO UPDATE分支时和当前行放在同一条语句的上下文里工作的整个“旧值读取 新值计算 写入”是一步完成的没有中间窗口。DO UPDATE分支里你可以做很多事情-- 不为空才更新 ON CONFLICT (user_id) DO UPDATE SET nickname COALESCE(EXCLUDED.nickname, user_profile.nickname), updated_at now();-- 只在特定条件下更新 ON CONFLICT (user_id) DO UPDATE SET status disabled, updated_at now() WHERE user_profile.status disabled;第二种写法里的WHERE是挂在DO UPDATE后面的意思是“只有满足这个条件时才执行更新”。如果不满足这条语句也会执行但不会真的更新数据。这个行为天然适合做“订单幂等”——同一个订单号第二次同步时直接跳过不用在应用层额外判断。2.3 多列唯一索引与 ON CONFLICT ON CONSTRAINT很多时候业务上的唯一性不是单字段而是多字段组合。比如订单明细表里一个订单号加一个商品编码才构成唯一维度。这时你在两个字段上建联合唯一索引CREATE UNIQUE INDEX idx_order_item ON order_detail (order_no, item_no);对应的ON CONFLICT就要把两列都写出来INSERT INTO order_detail (order_no, item_no, quantity, price) VALUES (SO20241001, ITEM-001, 2, 19.9) ON CONFLICT (order_no, item_no) DO UPDATE SET quantity order_detail.quantity EXCLUDED.quantity, price EXCLUDED.price;这里有个极容易被忽略的细节如果你只写ON CONFLICT (order_no)即使order_no本身也有一个普通索引PostgreSQL 一样会报“没有匹配的唯一约束”错误。联合唯一索引必须完整指定所有参与列的集合缺一不可。换句话说冲突目标的列集合必须和某个唯一索引的列集合完全一致。还有一种情况你不想记住索引对应哪些列可以直接用约束名INSERT INTO order_detail (order_no, item_no, quantity, price) VALUES (SO20241001, ITEM-001, 2, 19.9) ON CONFLICT ON CONSTRAINT idx_order_item DO UPDATE SET quantity order_detail.quantity EXCLUDED.quantity;我个人很少用约束名因为一旦某个 DBA 手滑删了旧约束重建新约束名字变了SQL 就得跟着改。列名写在代码里至少语义更稳一些。但如果你是在迁移脚本里批量生成 upsert通过约束名来写可能更省事看团队习惯。2.4 WHERE 谓词部分唯一索引的冲突目标有一种特殊情况业务逻辑里常做软删除记录删除时不真的删除而是把deleted_at置为非空同时要求“未删除状态下唯一”。比如用户邮箱地址只有deleted_at IS NULL的记录才要求邮箱唯一历史已删除记录可以存在相同邮箱。建索引的写法是CREATE UNIQUE INDEX idx_users_email_active ON users (email) WHERE deleted_at IS NULL;这时候你的ON CONFLICT也要跟上这个谓词INSERT INTO users (email, nickname) VALUES (xiaomingexample.com, 小明) ON CONFLICT (email) WHERE deleted_at IS NULL DO UPDATE SET nickname EXCLUDED.nickname;注意ON CONFLICT后面先写列名再写WHERE deleted_at IS NULL这正好对应了那个部分唯一索引的定义。如果你只写ON CONFLICT (email)PostgreSQL 会发现表上确实有email的唯一索引但它是个部分索引和你的冲突目标不匹配于是又报错。这个坑我在第五章还会详细讲因为这是我从实际支援的线上问题里看到频率最高的一个。3. 三个高频业务场景的完整 SQL 与返回行数利用光讲语法印象不深。我直接放三个我在真实项目里用过的场景每个都带上完整的 SQL 和返回行数的处理逻辑。3.1 场景一用户注册与资料更新合一业务需求第三方登录成功后用邮箱唯一性登录。如果用户第一次来插入一条如果来过更新一下最近登录时间和昵称。听起来要写两段逻辑其实一句话能做完WITH new_user AS ( INSERT INTO users (email, nickname, avatar, last_login_at) VALUES (xiaomingexample.com, 小明, https://cdn.example.com/avatar/1.png, now()) ON CONFLICT (email) DO UPDATE SET nickname EXCLUDED.nickname, avatar EXCLUDED.avatar, last_login_at EXCLUDED.last_login_at RETURNING id, (xmax 0) AS inserted ) SELECT id, inserted FROM new_user;这里有个小技巧RETURNING中通过xmax 0判断是插入还是更新。xmax是 PostgreSQL 行系统字段新插入的行xmax为 0而被更新过的行会携带旧事务信息。这个技巧在一些 ORM 的 upsert 实现里也用到过。不过它更偏底层如果你不想用也可以直接在应用层看RETURNING返回的行数。注意RETURNING拿不到“受影响行数”只拿得到行内容。想区分插入还是更新要么用xmax要么在更新值里故意加一个只在首次插入时非空的字段要么就干脆在应用侧先用唯一 ID 判断一次再决定调用哪个分支——不过这样性能差点。3.2 场景二订单表幂等写入电商场景里渠道方回调可能存在重复通知业务方拿到的订单号应该是唯一的。遇到重复回调时不能把已经发货的订单状态覆盖回“待支付”。利用DO UPDATE WHERE可以精确控制INSERT INTO orders (order_no, status, pay_amount, updated_at) VALUES (SO20241001001, PAID, 199.00, now()) ON CONFLICT (order_no) DO UPDATE SET status EXCLUDED.status, pay_amount EXCLUDED.pay_amount, updated_at now() WHERE orders.status IN (CREATED, PENDING_PAY) RETURNING *;这个WHERE的意思是只有当前订单还在“已创建”或“待支付”状态时才允许把状态改成“已支付”。如果订单已经“已发货”或“已取消”更新条件不成立整条INSERT语句没有实际修改任何数据RETURNING返回 0 行如果是普通插入返回 1 行。这个模式最大的价值是幂等逻辑不需要在应用层写if报错处理也大幅简化。把渠道回调、消息重试这类入口全部收口到一条 SQL 里天然防重。3.3 场景三计数器与每日汇总运营平台经常要统计每天每个商品的曝光量、点击量。传统做法是先查今天有没有这行有则加一没有则插入。并发下这个逻辑特别容易丢计数。用 UPSERT 一行解决INSERT INTO product_daily_stats (stat_date, product_id, view_count, click_count) VALUES (CURRENT_DATE, 10001, 1, 1) ON CONFLICT (stat_date, product_id) DO UPDATE SET view_count product_daily_stats.view_count EXCLUDED.view_count, click_count product_daily_stats.click_count EXCLUDED.click_count, updated_at now() RETURNING view_count;这个 SQL 在双十一这类高流量场景下实测很稳。核心原因就是我在第二章说的累加逻辑发生在数据库内部同一行记录上不存在“先读后写”的竞态窗口。你哪怕同一毫秒内收到 100 个加一请求它们彼此之间也会依次获取行锁最终数字一个都不少。我用它处理过日出量在 8000 万级别的埋点汇总表没有出现丢数据的情况。不过要提醒的是如果同一个(stat_date, product_id)上的并发冲突非常严重DO UPDATE会让后面的请求排队等行锁吞吐量可能反而不如“先插入后聚合”的方案。计数器场景适合低频或中频写入如果每秒几万次的递增都打在同一行PostgreSQL 的单行更新瓶颈是绕不过去的。这时应该考虑用HLL近似统计、多行分片计数或者直接把累加丢到 Redis 再批量落库。4. 冲突判定、锁等待与序列跳号不可见的底层行为INSERT ON CONFLICT写起来简单但它背后的执行路径至少牵扯到三件事唯一索引冲突检测、行锁获取与等待、序列值消耗。每一件都可能改变你对这条 SQL 的信任度。4.1 索引先行先检查再插入还是先插入再补救PostgreSQL 的ON CONFLICT并不是“先插入撞了唯一约束再从错误里恢复”的模式。它的执行计划里优化器会先定位到对应的唯一索引插入前就通过索引探测目标元组是否存在。这和你印象里 MySQL 的ON DUPLICATE KEY UPDATE先尝试插入碰到主键重复再转更新在物理过程上是有差异的。MySQL 遇到冲突时会在错误路径上做“半路折返”PostgreSQL 则更接近“走索引就找到了该走哪条分支”。这个差异对最终结果是透明的但对锁行为影响很大PostgreSQL 在探测到目标行存在时会直接尝试获取该行的排他锁FOR UPDATE级别的锁然后进入DO UPDATE分支。如果目标行不存在它就去插入新行——但这个“不存在”的判断和普通INSERT一样需要处理并发下同时插入所产生的唯一索引等待。所以在高并发 upsert 场景里你完全可能观察到两类锁等待一类是等待已存在行的行锁另一类是等唯一索引页锁来避免幻影插入。这并不是死锁是正常的串行化过程但从pg_stat_activity看它们都表现为wait_event transactionid或data_file_immediate_shared。4.2 死锁确实可能发生别因为是单条语句就放松很多同学以为“单条 UPSERT 语句原子执行不可能死锁”。这是误解。INSERT ... ON CONFLICT DO UPDATE内部要对多行目标加锁锁获取顺序在不同并发事务里可能交错。最经典的死锁场景是事务 A 先 upsert 行 1再 upsert 行 2事务 B 先 upsert 行 2再 upsert 行 1两条语句交叉等待对方持有的行锁PostgreSQL 检测到死锁直接中止其中一方。我的建议是如果在一个事务里连续执行多条“不同冲突目标”的 upsert尽量保证所有事务按照相同的行顺序处理。比如订单明细批量同步时先按order_no排序再统一逐条 upsert。这一点和“多行UPDATE要排序以避免死锁”的教训完全一致。4.3 序列跳号冲突也会烧掉 IDINSERT ON CONFLICT在发现冲突之前已经为SERIAL或IDENTITY列向序列要过新值了。一旦走DO UPDATE分支这个拿到手的新序列值不会还回去。如果你的表主键是自增 ID你会观察到最终记录数可能只有 1 万行但下一个自增 ID 已经到 1 万 5 千中间那些数字全是 upsert 冲突时“浪费”掉的。这是 PostgreSQL 序列的固有特性不是 bug。序列本身设计成“不回收、不重放”是为了并发下分配唯一性正确。对绝大多数业务来说主键空洞无伤大雅。但如果你的业务对主键连续有强制要求比如要拿 ID 当票据号展示给用户你就不适合用自增序列做主键应该考虑用UUID或者用一个单独的业务订单号列做唯一约束主键继续自增但只在插入成功后才暴露给用户。4.4 触发器与系统字段的交互DO UPDATE分支会像普通UPDATE一样触发行级UPDATE触发器包括BEFORE UPDATE和AFTER UPDATE。DO NOTHING分支则不会触发任何更新类触发器因为它根本没改数据。这一点在审计表设计里非常重要如果你的触发器里写了“每次更新都记录一条变更日志”那 upsert 冲突后走更新分支自然会产生一条审计日志但如果你在应用层误以为所有 upsert 都只会插一条审计数据就会出现“多出来”的记录。我接手过一个项目就是因为在订单表上加了个AFTER UPDATE触发器做状态流转日志结果批量 upsert 把状态从“待支付”改到“已支付”时日志表里的中间旧状态被覆盖导致对账时少了一环。另外RETURNING拿到的xmax等系统字段也要小心。xmax 0判断插入在绝大多数场景可靠但如果你的事务里先执行了其他写操作又立刻 upsert 同一行xmax的值可能不是你期望的那个 0。尽量不要依赖这种偏底层的判断来做复杂的业务分支。5. 我踩过的五个坑从部分索引不匹配到并发死锁的排查过程5.1 坑一部分索引写错冲突目标SQL 一分钟就报错我前面提过的软删除场景真正线上报错长这样ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification我看到这条错误时第一反应是查users表到底有没有email的唯一索引。用\d users一看索引分明在。再细看它是个部分索引CREATE UNIQUE INDEX idx_users_email_active ON users (email) WHERE deleted_at IS NULL;问题就出在这里。PostgreSQL 对冲突目标的匹配要求“冲突目标列集合 谓词”必须和某个唯一索引完全对得上。解决的办法是把ON CONFLICT的写法同步成INSERT INTO users (email, nickname) VALUES (xiaomingexample.com, 小明) ON CONFLICT (email) WHERE deleted_at IS NULL DO UPDATE SET nickname EXCLUDED.nickname;有几个同学问我“为什么 PostgreSQL 不自动帮我找到这个部分索引”因为同一个列上完全可能同时存在多个不同谓词的部分唯一索引比如一个管deleted_at IS NULL一个管email backup数据库不知道该用哪个。把选择权交给你是更安全的设计。5.2 坑二同一列上多个唯一约束目标不明确还有一种情况表里有主键又加了email唯一约束你写ON CONFLICT (email)没问题。但如果你写成ON CONFLICT DO NOTHING不带冲突目标PostgreSQL 在多个唯一约束都存在时会选择任意一个约束作为冲突判定标准。这种“隐式选择”在单次语句里可能没问题但一旦表结构变化或不同分片数据形态不同判定目标可能漂移。更明确的错误发生在你写了ON CONFLICT (id, email)但表上只有一个以id为单列的唯一索引和一个以email为单列的唯一索引没有(id, email)联合索引。数据库会直接报错。你不用猜错误信息非常直白。我的经验是永远显式写清楚冲突目标列集合永远别让数据库替你猜。5.3 坑三DO UPDATE 里忘了加 WHERE状态被“写回”旧值这是我同事踩过的一个真实事故。业务逻辑要求推送系统只改状态为 PENDING 的记录已 FAILED 的记录不能覆盖。他写的 SQLINSERT INTO push_task (task_id, status, retry_count, updated_at) VALUES (12345, PENDING, 1, now()) ON CONFLICT (task_id) DO UPDATE SET status EXCLUDED.status, retry_count push_task.retry_count 1, updated_at now();结果某个失败状态的推送任务被再次触发后状态被重置成了 PENDING整个重试队列全乱了。改正很简单加上条件ON CONFLICT (task_id) DO UPDATE SET status EXCLUDED.status, retry_count push_task.retry_count 1, updated_at now() WHERE push_task.status FAILED;这个坑的价值在于DO UPDATE的语义是“遇到冲突就更新”它不知道你的业务状态机是什么。你没写WHERE它就无脑覆盖。凡是状态机流转的 upsert务必把“当前状态允许转换到新状态”写进WHERE条件里。5.4 坑四并发重复调用导致的应用层死锁重试风暴我遇到过一例很蹊跷的死锁报告两条完全相同的 upsert 语句作用于同一个订单却被 PostgreSQL 判定为死锁。凭日志看它们都在等同一个行锁不应该构成环。后来用pg_stat_activity抓现场才发现问题不出在这两条 upsert 上而是事务里还有其他UPDATE语句把另一张表里的同一行也锁了一次。两条事务对两个表的加锁顺序不同upsert 只是恰好触发了等待链条。这次排查给我们的教训是死锁排查不能只看报错的最后一条语句要看整个事务的加锁顺序。我当时是这么做的先开log_min_messages debug1在日志里定位死锁详情用pg_stat_activity抓取pg_locks快照分析每把锁的持有者和等待者把事务里所有涉及排序、批量更新的语句全部按固定顺序重排对互斥的业务入口比如按订单号处理的任务队列加应用层串行化控制。修复后这个死锁再没出现过。如果你也在排查类似问题别一上来怀疑ON CONFLICT本身先看事务里有没有多张表的交叉更新。5.5 坑五批量 upsert 与 REPLICA 冲突流复制排查要点最后一个小众但很实际的坑。在一个主从复制架构里INSERT ON CONFLICT在备库回放时依然会执行同样的冲突检测。如果主库上的写入顺序与备库回放顺序不同备库可能临时看到更旧的数据从而在回放时“多走一次插入”或“多走一次更新”最终状态是对得上的但备库上短暂出现过不一致。如果你的迁移、归档任务在主库上大批量 upsert同时备库又在承载读流量你可能会观察到备库查询结果“串行异常”明明主库已更新备库还返回旧值。这其实是流复制本来就有的复制延迟但在 upsert 场景下延迟会被突发的索引写放大。规避方式还是老一套控制批量任务速度、使用synchronous_commit提高一致性等级、或者让读要求高的业务走主库。6. 与 MySQL 的 ON DUPLICATE KEY 对比以及 MERGE 的边界6.1 语法对照表迁移时先改这几个点很多团队从 MySQL 迁到 PostgreSQL第一个遇到的不适应就是 upsert 语法。两者表面功能相似细节差异却很大我直接列常用对照能力MySQLON DUPLICATE KEY UPDATEPostgreSQLINSERT ON CONFLICT基本语法INSERT ... VALUES ... ON DUPLICATE KEY UPDATE col VALUES(col)INSERT ... VALUES ... ON CONFLICT (col) DO UPDATE SET col EXCLUDED.col引用“待插入值”VALUES(col)EXCLUDED.col忽略冲突不支持DO NOTHING要写INSERT IGNOREON CONFLICT DO NOTHING指定冲突列不能指定按主键或唯一键自动判断可以显式ON CONFLICT (col)带条件更新只能在SET里用IF表达式直接写WHERE谓词部分索引不支持支持且有独立语义影响行数语义插入1更新2原地无变化0插入1更新1原地无变化1PostgreSQL 认为更新发生最后一行常被人忽略。如果你的应用依赖“update 返回多少行”来判断有没有实际变化迁移 PostgreSQL 后逻辑可能偏航。举个例子一个接口幂等期望是“重复请求不触发任何副作用返回 0 行变化”MySQL 恰好返回 0PostgreSQL 却会返回 1因为它在物理上确实执行了一次更新元组。你必须在 SQL 里额外写一个条件比如WHERE column EXCLUDED.column才能做到“没有变化就不更新”。6.2 行为差异谁更适合高并发下的精确控制MySQL 的ON DUPLICATE KEY UPDATE在面对“多个唯一键冲突”时是拿整条待插入数据去匹配任意一个唯一键只要其中一个撞了就转更新。这不一定是坏事但确实容易产生误判。比如一张表同时有id主键和email唯一键你插入的数据里id不同但email已存在MySQL 会更新旧行PostgreSQL 的ON CONFLICT (email)则明确只盯着email冲突不会把id的差异也卷进来。这个“精确到约束”的能力在复杂主数据合并场景里是巨大的优势。锁和等待的差异也更明显。PostgreSQL 因为显式指定冲突目标优化器可以直接命中对应的唯一索引来定位行加锁范围更集中。MySQL 的插入尝试在先、发现冲突在后的机制在高并发重复插入同一主键时会频繁进入死锁检测路径这也是很多从 MySQL 迁来的团队反馈“PostgreSQL 的 upsert 更稳”的一个原因。6.3 MERGE 的边界什么情况下别硬用 ON CONFLICTPostgreSQL 15 开始支持标准 SQL 的MERGE可以完成比INSERT ON CONFLICT更复杂的“按条件插入、更新、删除”操作。那是不是应该全面倒向MERGE我的判断是分场景单表简单“有则更、无则插”INSERT ON CONFLICT永远优先。它语法更短执行路径更明确而且RETURNING xmax这种小技巧也只有它支持得干净。多表数据合并、源表数据按条件路由到不同的目标表、或者要在更新后同时删除过期行再看MERGE。MERGE在 PostgreSQL 里的执行计划和锁行为仍在持续优化中不是每个版本都能拿到和手写INSERT ... JOIN一样的性能。生产环境用之前务必在真实数据量的副本上做EXPLAIN ANALYZE。我也遇到过有人把所有INSERT统一改成MERGE的团队结果性能反而不如普通INSERT。工具给多了不代表每个场景都该用。MERGE解决的是复杂条件合并的维护问题不是单纯替代 upsert。6.4 迁移与日常维护时我自己的一套选择标准最后分享一个我个人的实操判断方法算是在多次线上事故后沉淀出来的第一先确认这张表的唯一键是不是单列。是就大胆用ON CONFLICT (col)是复合唯一键就把所有列写全是部分唯一索引必须在ON CONFLICT后补上WHERE谓词。第二凡是涉及状态机流转的更新DO UPDATE SET后面必须带WHERE把“允许从哪些状态变过来”写死。没有例外。第三批量导入、ETL 同步这类场景别一条条调 upsert。先INSERT ... ON CONFLICT DO NOTHING把已存在的行排除掉再单独UPDATE需要变化的部分或者用COPY进临时表再INSERT ... SELECT ... ON CONFLICT。这个思路比逐行调用少很多锁等待和序列消耗实测能把全量同步时间缩短一半以上。第四如果想测你的 upsert 在并发下稳不稳不要用单表pgbench的默认脚本自己写一个带唯一键冲突的小脚本开 16 个并发连接各跑 1000 次把死锁和返回行数都打印出来。这一步能提前暴露很多问题。就实际使用来说我从 PostgreSQL 9.5 开始用INSERT ON CONFLICT到现在它已经是我写业务写入逻辑时的首选工具。语法稳定、行为可预期、文档充足比早期靠WHERE EXISTS拼出来的“伪 upsert”可靠太多。希望这篇内容能帮你把它的细节吃透少踩我当初踩过的那些坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑