
引言:ETL 中最头疼的上游数据同步
你有没有遇到过这种场景:
- 每天从上游系统拿到一份新的用户数据 CSV
- 需要用 Python 先查现有数据、再比对差异、然后分别执行 INSERT / UPDATE / DELETE
- 代码写了五十行起步,还经常漏掉边界情况(比如有用户被删了、有新用户加入、字段部分更新)
- 数据量大一点,pandas 读进来内存直接爆掉
这是 ETL 中最常见的「Upsert」需求。
传统做法要三张表来回 JOIN,逻辑复杂、容易出错。而 DuckDB 用一条 MERGE INTO 语句就能搞定所有操作——插入、更新、删除一步到位。
今天这篇文章,我会从基础到进阶,带你完整掌握 DuckDB 的 MERGE INTO 语法,并在最后给出如何把这个能力变成一个可售卖的数据产品的变现建议。
DuckDB MERGE INTO 核心原理
MERGE INTO 是 SQL 标准中用于实现 Upsert(Update + Insert)操作的语句。DuckDB 完整支持这一语法,并且做了针对性的优化:
- 原子性:整个 MERGE 是一个事务,要么全部成功,要么全部回滚
- 向量化执行:DuckDB 的列式存储让匹配逻辑比传统关系型数据库快数倍
- 零依赖:不需要额外的扩展或插件,开箱即用
语法结构
MERGE INTO target_table AS target
USING source_table AS source
ON target.key_column = source.key_column
WHEN MATCHED THEN
UPDATE SET col1 = source.col1, col2 = source.col2
WHEN NOT MATCHED THEN
INSERT (col1, col2) VALUES (source.col1, source.col2);
关键要素:
MERGE INTO指定目标表USING指定来源数据(可以是表、子查询、或 CTAS 结果)ON条件是匹配键WHEN MATCHED匹配成功时执行 UPDATEWHEN NOT MATCHED匹配失败时执行 INSERT
实战一:基础 UPSERT —— 新记录插入、已有记录更新
假设你每天收到一张用户快照表 daily_users,需要根据主键 user_id 更新到 users 表:
import duckdb
con = duckdb.connect("ecommerce.db")
# 创建目标表(初始状态)
con.execute("""
CREATE TABLE IF NOT EXISTS users AS SELECT * FROM (VALUES
(1, 'Alice', '[email protected]', '2024-01-01'),
(2, 'Bob', '[email protected]', '2024-01-01'),
(3, 'Carol', '[email protected]', '2024-01-01')
) t(user_id, name, email, updated_at)
""")
# 模拟今天的新数据
con.execute("""
CREATE TABLE IF NOT EXISTS daily_users AS SELECT * FROM (VALUES
(2, 'Bob_Jr', '[email protected]', '2024-09-24'),
(4, 'Dave', '[email protected]', '2024-09-24'),
(1, 'Alice_v2', '[email protected]', '2024-09-24')
) t(user_id, name, email, updated_at)
""")
# 一条 MERGE INTO 搞定所有操作
con.execute("""
MERGE INTO users AS target
USING daily_users AS source
ON target.user_id = source.user_id
WHEN MATCHED THEN
UPDATE SET
name = source.name,
email = source.email,
updated_at = source.updated_at
WHEN NOT MATCHED THEN
INSERT (user_id, name, email, updated_at)
VALUES (source.user_id, source.name, source.email, source.updated_at)
""")
# 查看结果
result = con.execute("SELECT * FROM users ORDER BY user_id").fetchdf()
print(result.to_string(index=False))
结果解读:
user_id=2(Bob → Bob_Jr)和user_id=1(邮箱变更)被更新user_id=4(Dave)被插入- 原来有的
user_id=3(Carol)保持不变
💡 关键洞察:MERGE INTO 是一个原子操作,整个过程不会丢失数据,也不会产生竞态条件。在分布式系统中,这就是为什么它比手动写 INSERT + UPDATE + DELETE 更安全。
实战二:带条件过滤的精准更新
不是所有字段都要无条件覆盖。比如只想在"name 发生变化"时才更新,避免不必要的写入:
MERGE INTO users AS target
USING daily_users AS source
ON target.user_id = source.user_id
WHEN MATCHED AND target.name <> source.name THEN
UPDATE SET
name = source.name,
updated_at = source.updated_at
WHEN NOT MATCHED THEN
INSERT (user_id, name, email, updated_at)
VALUES (source.user_id, source.name, source.email, source.updated_at);
AND target.name <> source.name 就是条件过滤器——只有满足条件的行才会触发 UPDATE,避免无意义的写入。这在数据量大的时候非常关键,可以减少 I/O 和锁竞争。
实战三:全量同步 + 软删除
实际业务中,上游数据可能删除了某些用户。用 DELETE 子句实现反向删除:
-- 先给 users 表加一个软删除标志
con.execute("ALTER TABLE users ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN DEFAULT FALSE")
MERGE INTO users AS target
USING daily_users AS source
ON target.user_id = source.user_id
WHEN MATCHED THEN
UPDATE SET
name = source.name,
email = source.email,
updated_at = source.updated_at
WHEN NOT MATCHED THEN
INSERT (user_id, name, email, updated_at)
VALUES (source.user_id, source.name, source.email, source.updated_at)
WHEN NOT MATCHED BY SOURCE AND target.is_deleted = FALSE THEN
UPDATE SET is_deleted = TRUE, updated_at = DATE '2024-09-24';
逻辑拆解:
MATCHED→ 存在即更新NOT MATCHED(源中有、目标中没有)→ 插入NOT MATCHED BY SOURCE(目标中有、源中没有)→ 标记为软删除
💡 软删除 vs 物理删除:软删除比物理删除更安全——数据还在,只是标记为失效,随时可以恢复。在商业场景中,这也很关键,因为客户的数据你可能需要在某个时间点回溯。
实战四:ON CONFLICT 简写 —— 纯 Upsert 场景
如果你更熟悉 PostgreSQL 语法,DuckDB 也支持另一种写法:
INSERT INTO users (user_id, name, email, updated_at)
SELECT user_id, name, email, updated_at
FROM daily_users
ON CONFLICT (user_id) DO UPDATE SET
name = excluded.name,
email = excluded.email,
updated_at = excluded.updated_at;
excluded 是 DuckDB 内置的伪表,代表被拒绝插入的行数据。这种写法简洁,适合只有插入和更新、不需要删除的场景。
智能更新:COALESCE 避免脏数据覆盖
当新旧数据都有值时,决定用哪个来源很关键。用 COALESCE 实现智能更新——只在源数据有值时才覆盖:
MERGE INTO users AS target
USING daily_users AS source
ON target.user_id = source.user_id
WHEN MATCHED THEN
UPDATE SET
name = COALESCE(source.name, target.name),
email = COALESCE(source.email, target.email),
updated_at = source.updated_at
WHEN NOT MATCHED THEN
INSERT (user_id, name, email, updated_at)
VALUES (source.user_id, source.name, source.email, source.updated_at);
这样即使上游数据有部分字段为空,也不会把目标表中的有效数据覆盖掉。这是生产环境中非常实用的技巧。
性能对比:MERGE INTO vs 传统 Python 方案
| 方式 | 代码行数 | 出错风险 | 可读性 | 内存占用(100万行) |
|---|---|---|---|---|
| 传统做法:SELECT + INSERT + UPDATE + DELETE | 20-40 行 | 高(边界 case 多) | 差 | 500MB+(pandas 全量加载) |
| DuckDB MERGE INTO | 5-10 行 | 低(原子操作) | 好 | 50MB(列式存储+谓词下推) |
| DuckDB INSERT … ON CONFLICT | 3-5 行 | 最低 | 最好 | 同 MERGE INTO |
核心优势总结:
- 原子性 — MERGE 是一个事务,要么全部成功,要么全部回滚
- 简洁 — 一行语句替代多步操作
- 性能 — DuckDB 内部优化了匹配逻辑,比手动写 JOIN 快得多
- 零内存膨胀 — 列式存储 + 流式处理,百万行数据轻松应对
常见陷阱与避坑指南
陷阱 1:别忘记 ON 条件中的索引
如果 user_id 没有索引,MERGE 会对全表做嵌套循环匹配,数据量大时极慢。建个主键或索引:
ALTER TABLE users ADD PRIMARY KEY (user_id);
-- 或者
CREATE INDEX idx_users_user_id ON users(user_id);
陷阱 2:NOT MATCHED BY SOURCE 的坑
NOT MATCHED BY SOURCE 会匹配目标表中所有源表没有的行。如果你只想删除「软删标志为 false」的行,记得加过滤条件,否则会误删历史归档数据:
-- ❌ 危险:会删除所有不在源表中的行
WHEN NOT MATCHED BY SOURCE THEN DELETE
-- ✅ 安全:只软删除未标记删除的行
WHEN NOT MATCHED BY SOURCE AND target.is_deleted = FALSE THEN
UPDATE SET is_deleted = TRUE, updated_at = CURRENT_TIMESTAMP
陷阱 3:更新字段的来源选择
当新旧数据都有值时,需要根据业务场景决定更新策略:
-- 策略 A:新数据优先(适合增量同步)
UPDATE SET name = source.name
-- 策略 B:新数据为空时才用旧的(适合部分更新)
UPDATE SET name = COALESCE(source.name, target.name)
-- 策略 C:始终保留最早的创建时间
UPDATE SET updated_at = GREATEST(source.updated_at, target.updated_at)
变现建议:从技能到收入
MERGE INTO 这个技能本身价值有限,但如果你把它包装成一个数据同步产品,就能产生真实收入:
方案 A:数据同步 SaaS 服务
面向电商、零售、餐饮等中小企业,提供每日/每周数据自动同步服务:
- 基础版:¥500/月,同步 1-2 个数据源,每日更新
- 专业版:¥1500/月,同步 5 个数据源,实时增量同步 + 软删除保护
- 企业版:¥3000/月,自定义同步规则 + API 对接 + 数据质量报告
一个自由分析师同时服务 10 个客户 = ¥5000-15000/月收入,系统自动化运行后每月维护时间 < 3 小时。
方案 B:一次性项目交付
为企业定制数据同步管道:
- 数据采集 → DuckDB 清洗 → MERGE INTO 增量更新 → 输出到 BI 工具
- 单项目 ¥3000-8000,后续维护费 ¥500-1000/月
- 标准化模板复用,边际成本几乎为零
方案 C:教程产品
把这套经验整理成付费教程:
- 入门教程:¥99,涵盖 MERGE INTO 基础语法和 3 个实战案例
- 进阶课程:¥299,包含完整 ETL 管道搭建 + 变现案例拆解
- 1v1 咨询:¥500/小时,针对企业具体场景定制方案
记住:你不是在卖 SQL 知识,你是在卖「数据不再混乱的自由」。 中小企业最痛的点不是没有数据,而是数据每天都在变、手动维护永远跟不上的焦虑。你的 MERGE INTO 解决方案,卖的是「安心」。
今晚行动
- 安装 DuckDB:
pip install duckdb - 创建一个测试数据库,复制上面的示例代码跑一遍
- 把你的业务数据(CSV 或数据库)作为 daily_users 表,跑一次 MERGE INTO
- 加上软删除逻辑,体验完整的数据同步流程
- 如果你是数据服务提供者,把这套流程包装成一个标准化的数据同步产品
一条 MERGE INTO,替代五十行 Python 比对代码。 增量同步的场景里,它是最高效的解决方案。
📌 收藏这篇文章,下次做数据同步时直接参考。 🔍 duckdblab.org 系统学习 DuckDB。