合并数据(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')。
语法与用法
完整语法骨架:
[ 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 }一个覆盖全部分支的最简示例:
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 VALUES | WHEN NOT MATCHED | 全部走默认值 |
UPDATE SET 列 = 表达式 | WHEN MATCHED / BY SOURCE | 右侧可引用源与目标 |
DELETE | WHEN MATCHED / BY SOURCE | 删除目标行 |
DO NOTHING | 任意分支 | 显式声明"这一类行不处理" |
详细说明
源数据从哪来
USING 后面可以是任何"能当表用"的东西,最灵活的是 CTE 与 VALUES:
-- 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 对同一目标行执行两次动作会直接失败:
-- 源表里 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);ERROR: MERGE command cannot affect row a second time
HINT: Ensure that not more than one source row matches any one target row.分支条件与顺序
WHEN 子句自上而下判断,第一个条件为真的分支胜出。因此细粒度条件必须写在前面:
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 也能带条件,用来过滤掉本不该插入的源行:
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 收尾,可以把"被跳过的行"显式声明出来(不写也可以,只是可读性更好):
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 最有价值的独占能力。同步场景里,源数据代表"当前应该存在的全集",目标表里源数据中已不存在的行,就是需要下架或清理的行。
-- 商品目录全量同步:源里没有的商品统一下架
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 分支同样可以带条件,实现"只下架,不物理删除"或"只删除某个时间点之前的":
WHEN NOT MATCHED BY SOURCE AND t.status = 'on_sale' THEN
UPDATE SET status = 'off_shelf', updated_at = now()注意 BY SOURCE 与 BY TARGET 是不同分支,可以同时出现:
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() 能直接拿到每行最终执行了什么,非常适合写审计日志。
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; action | sku | qty
----------+----------+-----
INSERT | SKU-1003 | 50
UPDATE | SKU-1001 | 10
UPDATE | SKU-1002 | 20
(3 rows)
MERGE 3把 RETURNING 结果作为 CTE 消费掉,可以在同一条语句里完成"变更 + 记账":
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 的取舍
三者能解决部分重叠的问题,但适用边界清晰:
-- 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 CONFLICT | UPDATE ... FROM + INSERT | MERGE |
|---|---|---|---|
| 语句条数 | 1 | 2 及以上 | 1 |
| 快照一致性 | 单条语句内一致 | 两条语句之间存在可见性缝隙 | 单条语句内一致 |
| 删除目标多余行 | 不支持 | 需额外 DELETE | 原生支持 |
| 分支条件 | 只有 WHERE 一种 | 灵活但分散 | 每个分支独立条件 |
| 冲突判定 | 唯一索引/约束 | ON 条件 | ON 条件 |
| 可读性 | 高(简单场景) | 中 | 高(复杂场景) |
并发与死锁
MERGE 在匹配到目标行时会加行锁。若多个会话以不同顺序处理同一批行,很容易互相等待形成死锁。
-- 会话 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);ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 54321.
HINT: See server log for query details.缓解手段:让所有会话按同一顺序处理行,并在业务层做重试。
-- 用 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。
-- 步骤 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);DROP TABLE
DROP TABLE
DROP TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 3
INSERT 0 4-- 步骤 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; 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-- 步骤 3:核对最终状态
SELECT sku, name, qty, price, status FROM inventory ORDER BY sku; 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)-- 步骤 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;INSERT 0 2-- 步骤 5:查询审计表 —— 第二次执行只有价格/数量变化的行被处理
SELECT action, sku, name, qty FROM inventory_audit ORDER BY id; action | sku | name | qty
--------+----------+----------+-----
UPDATE | SKU-1001 | 机械键盘 | 150
UPDATE | SKU-1002 | 无线鼠标 | 100
(2 rows)-- 步骤 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';MERGE 4
-- 行数照样是 4,但用 IS DISTINCT FROM 守卫后可以做到 0 行实际变更-- 步骤 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();MERGE 0
-- 数据已经完全一致,本次 MERGE 没有产生任何行版本变化-- 步骤 8:汇总本次同步结果
SELECT status, count(*) AS cnt, sum(qty) AS total_qty
FROM inventory
GROUP BY status
ORDER BY status; 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把源数据去重。
-- 错误:源里同一 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。
-- 只更新、不插入:源里的新品会被无声忽略
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是危险分支。 它会把"源数据里缺失"等同于"应该下架/删除"。源数据不完整时,这个分支会误伤大量正常数据。
-- 危险:源数据如果只包含增量而非全量,会把其他商品全部下架
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。
最佳实践
- 源侧先做一次"每键一行"的保证。 用
DISTINCT ON或GROUP BY去重,是MERGE不报错的前提。
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);- 每个
WHEN分支都写显式条件,并至少留一个兜底分支。 让"哪些行被处理、哪些被忽略"在 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;- 用
IS DISTINCT FROM做零写入幂等。 定时同步任务若不过滤"值没变"的行,会持续制造死元组与 WAL。
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()- 用
RETURNING merge_action()落审计。 同步任务最需要知道的正是"插入多少、更新多少、删除多少"。
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;WHEN NOT MATCHED BY SOURCE只在全量源上使用。 增量源配上这个分支等于批量误删,务必先确认源的语义是"全量快照"。避免用
MERGE替代简单的ON CONFLICT。 单行按主键写入,INSERT ... ON CONFLICT更简洁、开销更低。
-- 单行场景: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;- 关注死锁与重试。 多会话并发
MERGE时,应用层必须能识别 SQLSTATE40P01(deadlock detected)并重试。
-- 固定处理顺序,降低死锁概率
USING (SELECT * FROM staged_inventory ORDER BY sku) s ON t.sku = s.sku- 大表同步分批执行。 一次比对几百万行会长时间持锁并产生巨额 WAL,建议按业务键区间切分批次。
-- 每批 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);- 同步完成后
ANALYZE。 大批量变更会显著改变数据分布,统计信息滞后会影响后续执行计划。
VACUUM (ANALYZE) inventory;下一节
继续学习:批量导入导出(COPY)