Featured image of post DuckDB MERGE INTO 实战:一行 SQL 搞定增量更新,告别 Python 五十行比对代码

DuckDB MERGE INTO 实战:一行 SQL 搞定增量更新,告别 Python 五十行比对代码

手把手教你用 DuckDB 的 MERGE INTO 语句实现数据增量同步—— Upsert、条件更新、软删除一步到位,替代传统 Python 五十行比对代码。附完整商业变现建议。

DuckDB MERGE INTO 架构图

引言: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 匹配成功时执行 UPDATE
  • WHEN 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 + DELETE20-40 行高(边界 case 多)500MB+(pandas 全量加载)
DuckDB MERGE INTO5-10 行低(原子操作)50MB(列式存储+谓词下推)
DuckDB INSERT … ON CONFLICT3-5 行最低最好同 MERGE INTO

核心优势总结:

  1. 原子性 — MERGE 是一个事务,要么全部成功,要么全部回滚
  2. 简洁 — 一行语句替代多步操作
  3. 性能 — DuckDB 内部优化了匹配逻辑,比手动写 JOIN 快得多
  4. 零内存膨胀 — 列式存储 + 流式处理,百万行数据轻松应对

常见陷阱与避坑指南

陷阱 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 解决方案,卖的是「安心」。

今晚行动

  1. 安装 DuckDB:pip install duckdb
  2. 创建一个测试数据库,复制上面的示例代码跑一遍
  3. 把你的业务数据(CSV 或数据库)作为 daily_users 表,跑一次 MERGE INTO
  4. 加上软删除逻辑,体验完整的数据同步流程
  5. 如果你是数据服务提供者,把这套流程包装成一个标准化的数据同步产品

一条 MERGE INTO,替代五十行 Python 比对代码。 增量同步的场景里,它是最高效的解决方案。

📌 收藏这篇文章,下次做数据同步时直接参考。 🔍 duckdblab.org 系统学习 DuckDB。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计