Skip to content

合并数据(MERGE) ​

MERGE 用一个"数据源"去比对目标表,并按行决定插入、更新还是删除——它是幂等同步、数仓 SCD 场景的官方解法。相比手写"先 UPDATE 再 INSERT"的两段式逻辑,MERGE 只有一条语句、一次快照、一套判断,语义清晰且并发下更可控。本节讲透它的匹配规则、多分支写法、与 UPSERT 的取舍,以及几个必须知道的坑。

前置知识

阅读本节前,建议先了解:删除数据(DELETE)

基础概念 ​

MERGE 的执行模型是"逐行比对":把 USING 里的源数据(source)逐行拿去与目标表(target)按 ON 条件匹配,匹配上就执行 WHEN MATCHED 分支,匹配不上就执行 WHEN NOT MATCHED 分支。

分支触发条件可用动作
WHEN MATCHED目标表存在满足 ON 的行UPDATE / DELETE / DO NOTHING
WHEN NOT MATCHED(默认 BY TARGET)目标表没有满足 ON 的行INSERT / DO NOTHING
WHEN NOT MATCHED BY SOURCE目标表有行在源数据里找不到UPDATE / DELETE / DO NOTHING

几个关键术语:

  • 源(Source):USING 后面提供数据的表、子查询或 CTE。它可以是单表、JOIN 结果,也可以是 VALUES 列表。
  • 目标(Target):被插入/更新/删除的表,MERGE INTO 后跟的表名(可带别名)。
  • 匹配条件(ON):源与目标的关联表达式。它必须是布尔表达式,通常是等值关联。
  • WHEN NOT MATCHED BY SOURCE:这是 MERGE 相比 UPSERT 独有的能力——能处理"目标表里多出来的行"(例如源数据里已消失的商品,需要下架或删除)。
  • 一次匹配一次动作(Each row matched at most once):同一目标行只能被一个分支处理一次。若源数据里有两行同时匹配同一个目标行,会直接报错。
  • merge_action():RETURNING 中可用的函数,返回本行实际执行的动作('INSERT' / 'UPDATE' / 'DELETE')。

语法与用法 ​

完整语法骨架:

text
[ WITH with_query [, ...] ]
MERGE INTO [ ONLY ] target_table [ * ] [ [ AS ] target_alias ]
    USING data_source ON join_condition
    when_clause [...]
    [ RETURNING * | output_expression [ [ AS ] output_name ] [, ...] ]

其中 when_clause 为:
    WHEN MATCHED [ AND condition ] THEN { merge_update | merge_delete | DO NOTHING }
    WHEN NOT MATCHED [ AND condition ] THEN { merge_insert | DO NOTHING }
    WHEN NOT MATCHED BY SOURCE [ AND condition ] THEN { merge_update | merge_delete | DO NOTHING }

一个覆盖全部分支的最简示例:

sql
MERGE INTO inventory AS t
USING staged_inventory AS s
   ON t.sku = s.sku
WHEN MATCHED AND s.qty = 0 THEN
  DELETE
WHEN MATCHED THEN
  UPDATE SET qty = s.qty, price = s.price, updated_at = now()
WHEN NOT MATCHED THEN
  INSERT (sku, name, qty, price)
  VALUES (s.sku, s.name, s.qty, s.price)
WHEN NOT MATCHED BY SOURCE THEN
  UPDATE SET status = 'off_shelf', updated_at = now();

MERGE 与另外两种"写入并解决冲突"的方案对比:

方案适用场景源侧重复行能否删除目标多余行冲突判断依据
INSERT ... ON CONFLICT单行/小批量按唯一键覆盖允许(按下标顺序逐条)不能唯一索引/约束
UPDATE ... FROM + INSERT已确定要分开做两步会放大成多行更新不能手写关联条件
MERGE批量同步、需要增/改/删三合一会报错可以(BY SOURCE)ON 条件(任意表达式)

分支里可写的动作:

动作可用于说明
INSERT (列...) VALUES (...)WHEN NOT MATCHED引用源列写 s.列名
INSERT DEFAULT VALUESWHEN NOT MATCHED全部走默认值
UPDATE SET 列 = 表达式WHEN MATCHED / BY SOURCE右侧可引用源与目标
DELETEWHEN MATCHED / BY SOURCE删除目标行
DO NOTHING任意分支显式声明"这一类行不处理"

详细说明 ​

源数据从哪来 ​

USING 后面可以是任何"能当表用"的东西,最灵活的是 CTE 与 VALUES:

sql
-- 1. 直接来自一张暂存表
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (s.sku, s.qty);

-- 2. 来自 CTE(先在源侧聚合、去重、做映射)
WITH latest AS (
  SELECT DISTINCT ON (sku) sku, qty, price
  FROM price_feed
  ORDER BY sku, feed_time DESC
)
MERGE INTO inventory t
USING latest s ON t.sku = s.sku
WHEN MATCHED AND t.price IS DISTINCT FROM s.price THEN
  UPDATE SET price = s.price, updated_at = now()
WHEN NOT MATCHED THEN
  INSERT (sku, qty, price) VALUES (s.sku, s.qty, s.price);

CTE 是解决"源侧重复行"最常用的手段,因为 MERGE 对同一目标行执行两次动作会直接失败:

sql
-- 源表里 SKU-1001 有两行,会报错
INSERT INTO staged_inventory (sku, name, qty, price) VALUES
  ('SKU-1001', '机械键盘', 10, 399.00),
  ('SKU-1001', '机械键盘', 20, 399.00);

MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (s.sku, s.qty);
text
ERROR:  MERGE command cannot affect row a second time
HINT:  Ensure that not more than one source row matches any one target row.

分支条件与顺序 ​

WHEN 子句自上而下判断,第一个条件为真的分支胜出。因此细粒度条件必须写在前面:

sql
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
-- 先处理特殊情况
WHEN MATCHED AND s.qty = 0 THEN
  DELETE
-- 再处理常规更新
WHEN MATCHED THEN
  UPDATE SET qty = s.qty, updated_at = now()
WHEN NOT MATCHED THEN
  INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price);

WHEN NOT MATCHED 也能带条件,用来过滤掉本不该插入的源行:

sql
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
-- 只插入有效商品,软删除标记的行直接跳过
WHEN NOT MATCHED AND s.deleted = false THEN
  INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price);

再配一个 DO NOTHING 收尾,可以把"被跳过的行"显式声明出来(不写也可以,只是可读性更好):

sql
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED AND s.deleted = false THEN
  INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price)
WHEN NOT MATCHED THEN
  DO NOTHING;   -- 源里已删除的商品:什么都不做

WHEN NOT MATCHED BY SOURCE:处理"目标侧多出来的行" ​

这是 MERGE 最有价值的独占能力。同步场景里,源数据代表"当前应该存在的全集",目标表里源数据中已不存在的行,就是需要下架或清理的行。

sql
-- 商品目录全量同步:源里没有的商品统一下架
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN
  UPDATE SET qty = s.qty, price = s.price, status = 'on_sale', updated_at = now()
WHEN NOT MATCHED THEN
  INSERT (sku, name, qty, price, status)
  VALUES (s.sku, s.name, s.qty, s.price, 'on_sale')
WHEN NOT MATCHED BY SOURCE THEN
  UPDATE SET status = 'off_shelf', updated_at = now();

BY SOURCE 分支同样可以带条件,实现"只下架,不物理删除"或"只删除某个时间点之前的":

sql
WHEN NOT MATCHED BY SOURCE AND t.status = 'on_sale' THEN
  UPDATE SET status = 'off_shelf', updated_at = now()

注意 BY SOURCE 与 BY TARGET 是不同分支,可以同时出现:

sql
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (s.sku, s.qty)
WHEN NOT MATCHED BY SOURCE THEN UPDATE SET status = 'off_shelf';

RETURNING 与 merge_action() ​

MERGE 支持 RETURNING,配合 merge_action() 能直接拿到每行最终执行了什么,非常适合写审计日志。

sql
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (s.sku, s.qty)
RETURNING merge_action() AS action, t.sku, t.qty;
text
  action  |   sku    | qty
----------+----------+-----
 INSERT   | SKU-1003 |  50
 UPDATE   | SKU-1001 |  10
 UPDATE   | SKU-1002 |  20
(3 rows)

MERGE 3

把 RETURNING 结果作为 CTE 消费掉,可以在同一条语句里完成"变更 + 记账":

sql
WITH changed AS (
  MERGE INTO inventory t
  USING staged_inventory s ON t.sku = s.sku
  WHEN MATCHED THEN UPDATE SET qty = s.qty, updated_at = now()
  WHEN NOT MATCHED THEN INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price)
  RETURNING merge_action() AS action, t.sku, t.qty
)
INSERT INTO inventory_audit (action, sku, qty, changed_at)
SELECT action, sku, qty, now() FROM changed;

MERGE、UPSERT 与 UPDATE ... FROM 的取舍 ​

三者能解决部分重叠的问题,但适用边界清晰:

sql
-- UPSERT:按唯一键逐条覆盖,最适合"小批量、有唯一索引、只插或改"
INSERT INTO inventory (sku, name, qty, price)
VALUES ('SKU-1001', '机械键盘', 10, 399.00)
ON CONFLICT (sku) DO UPDATE
SET qty = EXCLUDED.qty, price = EXCLUDED.price;

-- UPDATE ... FROM + INSERT:需要分流、且更新逻辑复杂时更直观
UPDATE inventory t SET qty = s.qty
FROM staged_inventory s WHERE t.sku = s.sku;
INSERT INTO inventory (sku, name, qty, price)
SELECT s.sku, s.name, s.qty, s.price FROM staged_inventory s
WHERE NOT EXISTS (SELECT 1 FROM inventory t WHERE t.sku = s.sku);

-- MERGE:一次比对完成增/改/删,分支表达能力强
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price)
WHEN NOT MATCHED BY SOURCE THEN UPDATE SET status = 'off_shelf';
判断维度INSERT ... ON CONFLICTUPDATE ... FROM + INSERTMERGE
语句条数12 及以上1
快照一致性单条语句内一致两条语句之间存在可见性缝隙单条语句内一致
删除目标多余行不支持需额外 DELETE原生支持
分支条件只有 WHERE 一种灵活但分散每个分支独立条件
冲突判定唯一索引/约束ON 条件ON 条件
可读性高(简单场景)中高(复杂场景)

并发与死锁 ​

MERGE 在匹配到目标行时会加行锁。若多个会话以不同顺序处理同一批行,很容易互相等待形成死锁。

sql
-- 会话 A 与 B 同时执行,若源数据顺序不同,可能死锁
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price);
text
ERROR:  deadlock detected
DETAIL:  Process 12345 waits for ShareLock on transaction 67890; blocked by process 54321.
HINT:  See server log for query details.

缓解手段:让所有会话按同一顺序处理行,并在业务层做重试。

sql
-- 用 ORDER BY 固定处理顺序(在源侧排序),降低死锁概率
MERGE INTO inventory t
USING (SELECT sku, name, qty, price FROM staged_inventory ORDER BY sku) s
   ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price);

另外要注意:MERGE 的并发语义不是"锁住整批行",而是逐行加锁;因此边匹配边改的过程中,其他事务仍可能插入新行造成唯一约束冲突,应用需准备重试路径。

实战示例 ​

下面用一个"商品目录每日全量同步"的完整流程演示 MERGE 的三个分支与 RETURNING。

sql
-- 步骤 1:准备数据
DROP TABLE IF EXISTS inventory_audit;
DROP TABLE IF EXISTS inventory;
DROP TABLE IF EXISTS staged_inventory;

CREATE TABLE inventory (
  sku        text PRIMARY KEY,
  name       text          NOT NULL,
  qty        int           NOT NULL DEFAULT 0 CHECK (qty >= 0),
  price      numeric(12,2) NOT NULL DEFAULT 0,
  status     text          NOT NULL DEFAULT 'on_sale',
  updated_at timestamptz   NOT NULL DEFAULT now()
);

CREATE TABLE staged_inventory (
  sku     text PRIMARY KEY,
  name    text NOT NULL,
  qty     int  NOT NULL,
  price   numeric(12,2) NOT NULL,
  deleted boolean NOT NULL DEFAULT false
);

CREATE TABLE inventory_audit (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  action     text NOT NULL,
  sku        text NOT NULL,
  name       text NOT NULL,
  qty        int  NOT NULL,
  changed_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO inventory (sku, name, qty, price, status) VALUES
  ('SKU-1001', '机械键盘', 120, 399.00, 'on_sale'),
  ('SKU-1002', '无线鼠标',  80, 119.00, 'on_sale'),
  ('SKU-1004', '显示器支架', 30, 199.00, 'on_sale');

-- 源数据:1002 数量变化,1003 是新品,1004 已下架消失,1005 已标记删除
INSERT INTO staged_inventory (sku, name, qty, price, deleted) VALUES
  ('SKU-1001', '机械键盘',  150, 379.00, false),
  ('SKU-1002', '无线鼠标',  100, 109.00, false),
  ('SKU-1003', 'USB 集线器', 200,  89.00, false),
  ('SKU-1005', '已淘汰商品',   0,   0.00, true);
text
DROP TABLE
DROP TABLE
DROP TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 3
INSERT 0 4
sql
-- 步骤 2:执行全量同步(增 / 改 / 下架 / 忽略已删除源行)
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED AND s.deleted THEN
  DO NOTHING
WHEN MATCHED THEN
  UPDATE SET qty = s.qty, price = s.price, name = s.name,
             status = 'on_sale', updated_at = now()
WHEN NOT MATCHED AND s.deleted = false THEN
  INSERT (sku, name, qty, price, status)
  VALUES (s.sku, s.name, s.qty, s.price, 'on_sale')
WHEN NOT MATCHED BY SOURCE THEN
  UPDATE SET status = 'off_shelf', updated_at = now()
RETURNING merge_action() AS action, t.sku, t.name, t.qty, t.status;
text
  action  |   sku    |    name    | qty |  status
----------+----------+------------+-----+-----------
 UPDATE   | SKU-1001 | 机械键盘   | 150 | on_sale
 UPDATE   | SKU-1002 | 无线鼠标   | 100 | on_sale
 INSERT   | SKU-1003 | USB 集线器 | 200 | on_sale
 UPDATE   | SKU-1004 | 显示器支架 |  30 | off_shelf
(4 rows)

MERGE 4
sql
-- 步骤 3:核对最终状态
SELECT sku, name, qty, price, status FROM inventory ORDER BY sku;
text
   sku    |    name    | qty | price  |  status
----------+------------+-----+--------+-----------
 SKU-1001 | 机械键盘   | 150 | 379.00 | on_sale
 SKU-1002 | 无线鼠标   | 100 | 109.00 | on_sale
 SKU-1003 | USB 集线器 | 200 |  89.00 | on_sale
 SKU-1004 | 显示器支架 |  30 | 199.00 | off_shelf
(4 rows)
sql
-- 步骤 4:把变更写入审计表(MERGE 结果直接作为 CTE 消费)
WITH changed AS (
  MERGE INTO inventory t
  USING staged_inventory s ON t.sku = s.sku
  WHEN MATCHED AND s.deleted THEN
    DO NOTHING
  WHEN MATCHED THEN
    UPDATE SET qty = s.qty, updated_at = now()
  WHEN NOT MATCHED AND s.deleted = false THEN
    INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price)
  RETURNING merge_action() AS action, t.sku, t.name, t.qty
)
INSERT INTO inventory_audit (action, sku, name, qty)
SELECT action, sku, name, qty FROM changed;
text
INSERT 0 2
sql
-- 步骤 5:查询审计表 —— 第二次执行只有价格/数量变化的行被处理
SELECT action, sku, name, qty FROM inventory_audit ORDER BY id;
text
 action |   sku    |   name   | qty
--------+----------+----------+-----
 UPDATE | SKU-1001 | 机械键盘 | 150
 UPDATE | SKU-1002 | 无线鼠标 | 100
(2 rows)
sql
-- 步骤 6:验证幂等性 —— 重复执行同样的 MERGE,不会有额外变更
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED AND s.deleted THEN
  DO NOTHING
WHEN MATCHED THEN
  UPDATE SET qty = s.qty, price = s.price, name = s.name,
             status = 'on_sale'
WHEN NOT MATCHED AND s.deleted = false THEN
  INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price)
WHEN NOT MATCHED BY SOURCE THEN
  UPDATE SET status = 'off_shelf';
text
MERGE 4
-- 行数照样是 4,但用 IS DISTINCT FROM 守卫后可以做到 0 行实际变更
sql
-- 步骤 7:用 IS DISTINCT FROM 抑制无意义更新,做到真正的幂等零写入
MERGE INTO inventory t
USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED AND s.deleted THEN
  DO NOTHING
WHEN MATCHED AND (t.qty, t.price, t.status) IS DISTINCT FROM
                 (s.qty, s.price, 'on_sale') THEN
  UPDATE SET qty = s.qty, price = s.price, status = 'on_sale', updated_at = now()
WHEN NOT MATCHED AND s.deleted = false THEN
  INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price)
WHEN NOT MATCHED BY SOURCE AND t.status <> 'off_shelf' THEN
  UPDATE SET status = 'off_shelf', updated_at = now();
text
MERGE 0
-- 数据已经完全一致,本次 MERGE 没有产生任何行版本变化
sql
-- 步骤 8:汇总本次同步结果
SELECT status, count(*) AS cnt, sum(qty) AS total_qty
FROM inventory
GROUP BY status
ORDER BY status;
text
  status   | cnt | total_qty
-----------+-----+-----------
 off_shelf |   1 |        30
 on_sale   |   3 |       450
(2 rows)

上面的流程覆盖了 MERGE 的关键能力:分支条件与顺序、WHEN NOT MATCHED BY SOURCE 处理目标侧多余行、RETURNING + merge_action() 审计、以及用 IS DISTINCT FROM 做到幂等零写入。

注意事项 ​

  • 同一目标行不能被处理两次。 源侧有重复键时会直接报 MERGE command cannot affect row a second time。务必先用 DISTINCT ON / GROUP BY 把源数据去重。
sql
-- 错误:源里同一 sku 出现两次
MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty;

-- 正确:先聚合去重
MERGE INTO inventory t
USING (SELECT sku, max(qty) AS qty FROM staged_inventory GROUP BY sku) s
   ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty;
  • WHEN 分支自上而下匹配,第一个命中即生效。 把特殊条件放在前面,否则会被通用分支"吃掉"。

  • 没有匹配上的源行不会报错。 若你只写了 WHEN MATCHED,那些"目标里不存在的源行"会被静默丢弃。想插入就必须写 WHEN NOT MATCHED。

sql
-- 只更新、不插入:源里的新品会被无声忽略
MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty;
  • ON 条件写法不当会放大匹配。 若 ON 是范围或不等式条件,一行可能匹配多行,同样触发"处理两次"错误。

  • MERGE 不是原子"整表替换"。 它逐行加锁、逐行判断,中途失败会回滚整条语句,但不代表不存在死锁与锁等待。

  • 多会话并发执行可能死锁。 让各会话按相同顺序处理行,并准备好重试逻辑。

  • WHEN NOT MATCHED BY SOURCE 是危险分支。 它会把"源数据里缺失"等同于"应该下架/删除"。源数据不完整时,这个分支会误伤大量正常数据。

sql
-- 危险:源数据如果只包含增量而非全量,会把其他商品全部下架
MERGE INTO inventory t USING incremental_feed s ON t.sku = s.sku
WHEN NOT MATCHED BY SOURCE THEN UPDATE SET status = 'off_shelf';
  • MERGE 不会绕过约束。 唯一约束、外键、CHECK 一样生效;INSERT 分支撞到唯一键会直接报错,不会自动转为 UPDATE(这与 ON CONFLICT 不同)。

  • MERGE 里的 UPDATE 同样产生死元组。 高频全量同步必须配合 IS DISTINCT FROM 守卫与合理的 autovacuum 设置。

  • MERGE 的完整 RETURNING 支持需 PostgreSQL 17+。 在更早的版本上 MERGE 不能 RETURNING。

最佳实践 ​

  1. 源侧先做一次"每键一行"的保证。 用 DISTINCT ON 或 GROUP BY 去重,是 MERGE 不报错的前提。
sql
MERGE INTO inventory t
USING (
  SELECT DISTINCT ON (sku) sku, name, qty, price
  FROM staged_inventory
  ORDER BY sku, updated_at DESC     -- 同一 sku 取最新一条
) s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty, price = s.price
WHEN NOT MATCHED THEN INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price);
  1. 每个 WHEN 分支都写显式条件,并至少留一个兜底分支。 让"哪些行被处理、哪些被忽略"在 SQL 里一目了然。
sql
MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED AND s.deleted THEN DO NOTHING
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED AND s.deleted = false THEN INSERT (sku, qty) VALUES (s.sku, s.qty)
WHEN NOT MATCHED THEN DO NOTHING;
  1. 用 IS DISTINCT FROM 做零写入幂等。 定时同步任务若不过滤"值没变"的行,会持续制造死元组与 WAL。
sql
WHEN MATCHED AND (t.qty, t.price) IS DISTINCT FROM (s.qty, s.price) THEN
  UPDATE SET qty = s.qty, price = s.price, updated_at = now()
  1. 用 RETURNING merge_action() 落审计。 同步任务最需要知道的正是"插入多少、更新多少、删除多少"。
sql
WITH changed AS (
  MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
  WHEN MATCHED THEN UPDATE SET qty = s.qty
  WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (s.sku, s.qty)
  RETURNING merge_action() AS action, t.sku
)
SELECT action, count(*) FROM changed GROUP BY action;
  1. WHEN NOT MATCHED BY SOURCE 只在全量源上使用。 增量源配上这个分支等于批量误删,务必先确认源的语义是"全量快照"。

  2. 避免用 MERGE 替代简单的 ON CONFLICT。 单行按主键写入,INSERT ... ON CONFLICT 更简洁、开销更低。

sql
-- 单行场景:UPSERT 更合适
INSERT INTO inventory (sku, name, qty, price)
VALUES ('SKU-1001', '机械键盘', 150, 379.00)
ON CONFLICT (sku) DO UPDATE SET qty = EXCLUDED.qty, price = EXCLUDED.price;
  1. 关注死锁与重试。 多会话并发 MERGE 时,应用层必须能识别 SQLSTATE 40P01(deadlock detected)并重试。
sql
-- 固定处理顺序,降低死锁概率
USING (SELECT * FROM staged_inventory ORDER BY sku) s ON t.sku = s.sku
  1. 大表同步分批执行。 一次比对几百万行会长时间持锁并产生巨额 WAL,建议按业务键区间切分批次。
sql
-- 每批 10000 行,按 sku 前两位切分(示意)
MERGE INTO inventory t
USING (SELECT * FROM staged_inventory WHERE sku LIKE 'SKU-1%') s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, name, qty, price) VALUES (s.sku, s.name, s.qty, s.price);
  1. 同步完成后 ANALYZE。 大批量变更会显著改变数据分布,统计信息滞后会影响后续执行计划。
sql
VACUUM (ANALYZE) inventory;

下一节

继续学习:批量导入导出(COPY)

参考链接 ​