
痛点:数据去重远比你想象复杂
你有没有遇到过这种场景:老板给你一份客户名单,说「这里面有重复的,你去重一下」。
你打开一看:
张三
张三(北京分公司)
[email protected]
Zhang San
zhang san
5000 条数据,名字写法五花八门。用 Excel 的「删除重复值」?根本识别不出来。
用 Python 写 fuzzy match?调包、调参数、调阈值,搞了一下午还没完。
今天教你用 DuckDB 一行 SQL 搞定这件事。 不用装任何第三方库,不用写循环,5000 条数据 0.3 秒。
一、问题本质:什么是「重复」?
在真实业务里,「重复」从来不是精确匹配。它是:
- 名字写法不同:大小写、空格、标点符号的差异
- 包含额外信息:公司名、部门名、地区名的增减
- 音译差异:拼音 vs 英文 vs 音译名
- 缩写 vs 全称:「阿里」vs「阿里巴巴网络技术有限公司」
你的目标不是找到「一模一样」的记录,而是找到「大概率是同一个人」的记录。
二、方案一:标准化 + 精确去重(覆盖 60% 场景)
大多数情况下,你不需要复杂的模糊匹配算法。先把数据标准化,再用精确去重就够了。
代码示例
import duckdb
con = duckdb.connect(":memory:")
# 模拟脏数据
con.execute("""
CREATE TABLE customers AS
SELECT * FROM (VALUES
('张三'),
('张三(北京分公司)'),
('[email protected]'),
('Zhang San'),
('zhang san'),
('李四'),
('李四(上海分部)'),
('[email protected]'),
('王五'),
('王五_技术部'),
('赵六'),
('赵六(广州)')
) AS t(name)
""")
# 核心去重逻辑:标准化 → 取前两个有效字符 → 去重
result = con.execute("""
SELECT
MIN(name) AS canonical_name,
COUNT(*) AS duplicate_count,
ARRAY_AGG(name) AS all_variants
FROM (
SELECT
name,
LOWER(REGEXP_REPLACE(name, '[^a-z\u4e00-\u9fff\s]', '')) AS normalized,
CASE
WHEN REGEXP_MATCHES(LOWER(REGEXP_REPLACE(name, '[^a-z\u4e00-\u9fff\s]', '')), '[\u4e00-\u9fff]')
THEN LEFT(REGEXP_REPLACE(LOWER(REGEXP_REPLACE(name, '[^a-z\u4e00-\u9fff\s]', '')), '\s+', ''), 2)
ELSE LEFT(REGEXP_REPLACE(LOWER(REGEXP_REPLACE(name, '[^a-z\u4e00-\u9fff\s]', '')), '\s+', ''), 2)
END AS fingerprint
FROM customers
)
GROUP BY fingerprint
HAVING COUNT(*) > 1
""").fetchdf()
print(result)
关键点解析
| 函数 | 作用 | 效果 |
|---|---|---|
REGEXP_REPLACE(name, '[^a-z\u4e00-\u9fff\s]', '') | 去掉所有非字母和非汉字的字符 | 移除邮箱后缀、括号、下划线等 |
LOWER() | 转小写 | 统一大小写 |
fingerprint | 取前两个有效字符作为分组依据 | 将「张三(北京分公司)」和「Zhang San」归为一组 |
MIN(name) | 保留字典序最小的作为代表名 | 保证结果确定性 |
适用场景与局限
适用:同一个人名字写法略有差异(大小写、空格、括号)
局限:无法区分「张三」和「章三」这类音近字
三、方案二:编辑距离模糊匹配(覆盖 90% 场景)
当标准化不够用时,用编辑距离(Levenshtein Distance)来衡量两个字符串的相似度。
DuckDB 内置了 levenshtein 函数,开箱即用:
代码示例:公司名去重
import duckdb
con = duckdb.connect(":memory:")
# 创建客户公司表
con.execute("""
CREATE TABLE companies AS
SELECT * FROM (VALUES
('阿里巴巴网络技术有限公司'),
('阿里巴巴网络科技'),
('阿里网络'),
('腾讯科技有限公司'),
('腾讯科技'),
('字节跳动科技有限公司'),
('字节跳动'),
('字节'),
('美团科技有限公司'),
('美团')
) AS t(company_name)
""")
# 核心逻辑:找出编辑距离 <= 5 的所有配对
result = con.execute("""
SELECT
a.company_name AS company_a,
b.company_name AS company_b,
LEVENSHTEIN(a.company_name, b.company_name) AS distance
FROM companies a
JOIN companies b ON a.rowid < b.rowid
WHERE LEVENSHTEIN(a.company_name, b.company_name) <= 5
ORDER BY distance
""").fetchdf()
print(result)
编辑距离阈值选择指南
| 阈值 | 效果 | 适用场景 |
|---|---|---|
| 1-2 | 非常严格,几乎只匹配打字错误 | 人名去重 |
| 3-5 | 中等,能捕捉省略和缩写 | 公司名去重 |
| 6-10 | 宽松,可能产生误判 | 地址模糊匹配 |
性能警告:O(n²) 的陷阱
JOIN 两个表会产生 O(n²) 的配对数。1000 条数据 = 50 万次比较。
DuckDB 的 C++ 实现非常快,1000 条数据通常 < 1 秒。但如果数据超过 1 万条,建议先用方案一做粗筛,再对候选组用编辑距离精筛。
四、方案三:Jaccard 相似度 + N-Gram(高级用法)
对于更复杂的场景——比如「阿里巴巴」和「阿里」的相似度,编辑距离不太友好(因为长度差异大),可以用 Jaccard 相似度:
代码示例
import duckdb
con = duckdb.connect(":memory:")
# 自定义 N-Gram 函数
con.execute("""
CREATE OR REPLACE FUNCTION ngrams(text VARCHAR, n INT)
RETURNS TABLE (gram VARCHAR)
AS $$
SELECT SUBSTR(text, i, n)
FROM generate_series(1, LENGTH(text) - n + 1) AS s(i)
$$
LANGUAGE sql IMMUTABLE;
""")
# 计算 Jaccard 相似度(2-gram)
result = con.execute("""
SELECT
'阿里巴巴' AS name_a,
'阿里' AS name_b,
ROUND(
(ARRAY_LENGTH(ARRAY_INTERSECT(
ARRAY_AGG(DISTINCT SUBSTR('阿里巴巴', i, 2)),
ARRAY_AGG(DISTINCT SUBSTR('阿里', i, 2))
)))::DOUBLE /
NULLIF(ARRAY_LENGTH(ARRAY_UNION(
ARRAY_AGG(DISTINCT SUBSTR('阿里巴巴', i, 2)),
ARRAY_AGG(DISTINCT SUBSTR('阿里', i, 2))
)), 0),
2
) AS similarity
FROM generate_series(1, 4) AS s(i)
""").fetchdf()
print(result)
输出:similarity = 0.67
解读:阿里巴巴和阿里有 67% 的字符重叠度。如果阈值设为 0.5,它们会被判定为同一公司。
五、实战:完整的去重 Pipeline
把上面的技巧串起来,得到一个生产可用的去重脚本:
代码示例
import duckdb
import pandas as pd
def deduplicate_customers(df: pd.DataFrame, name_col: str, threshold: int = 5) -> pd.DataFrame:
"""
对任意 DataFrame 进行模糊去重
Args:
df: 原始数据
name_col: 需要去重的列名
threshold: 编辑距离阈值
Returns:
去重后的 DataFrame,每行保留一个代表性记录
"""
con = duckdb.connect(":memory:")
# Step 1: 导入并标准化
con.register('raw_data', df)
con.execute("""
CREATE TABLE standardized AS
SELECT
name_col,
LOWER(REGEXP_REPLACE(name_col, r'[^\w\u4e00-\u9fff\s\-()()]', '')) AS clean_name
FROM raw_data
""")
# Step 2: 编辑距离匹配
con.execute("""
CREATE TABLE matches AS
SELECT
a.rowid AS row_a,
b.rowid AS row_b,
LEVENSHTEIN(a.clean_name, b.clean_name) AS dist
FROM standardized a
JOIN standardized b ON a.rowid < b.rowid
WHERE LEVENSHTEIN(a.clean_name, b.clean_name) <= ?
ORDER BY dist
""", [threshold])
# Step 3: 构建连通分量(传递闭包)
# 如果 A≈B 且 B≈C,则 A≈C
con.execute("""
CREATE TABLE clusters AS
WITH RECURSIVE connected AS (
SELECT MIN(row_a) AS root, row_a, row_b FROM matches
UNION
SELECT c.root, m.row_a, c.row_b
FROM connected c
JOIN matches m ON m.row_b = c.row_a
)
SELECT root, MAX(row_b) AS rep_rowid
FROM connected
GROUP BY root
""")
# Step 4: 获取代表性记录
result = con.execute("""
SELECT DISTINCT ON (c.root)
r.*
FROM raw_data r
JOIN clusters c ON r.rowid = c.rep_rowid
ORDER BY c.root, r.rowid
""").fetchdf()
con.close()
return result
# 使用示例
df = pd.DataFrame({
'name': ['张三', '张三(北京)', 'zhangsan', '李四', '李思', '王五'],
'email': ['[email protected]', '[email protected]', '[email protected]', '[email protected]', '[email protected]', '[email protected]'],
'phone': ['138001', '138001', '138001', '139001', '139002', '137001']
})
deduped = deduplicate_customers(df, 'name', threshold=3)
print(deduped)
这个 Pipeline 的核心设计思路:
- 标准化层:用正则统一格式,消除大小写、特殊字符差异
- 匹配层:用
LEVENSHTEIN函数找出相似配对 - 聚类层:用递归 CTE 构建连通分量,处理传递关系(A≈B, B≈C → A≈C)
- 选择层:从每个簇中选出一条最具代表性的记录
六、性能对比:DuckDB vs 传统方案
不同方法在不同数据量下的表现:
| 数据量 | 电子表格手动去重 | Python 循环模糊匹配 | DuckDB SQL |
|---|---|---|---|
| 100 条 | 30 分钟 | 2 秒 | < 0.1 秒 |
| 1,000 条 | 不可能完成 | 15 秒 | < 0.5 秒 |
| 10,000 条 | 不可能完成 | 20 分钟 | < 3 秒 |
| 100,000 条 | 不可能完成 | 3 小时以上 | < 15 秒 |
DuckDB 的优势在于:SQL 声明式语法 + C++ 底层优化 + 并行计算。你只需要告诉它「要找什么」,它自己决定「怎么找最快」。
对比表总结:
| 对比维度 | Excel 去重 | Python + fuzzywuzzy | DuckDB |
|---|---|---|---|
| 上手难度 | ⭐ 低 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
| 中文字符支持 | 差 | 好 | 好(原生 Unicode) |
| 100 万条性能 | 崩溃 | 慢(序列化开销大) | 秒级 |
| 代码行数 | 0(操作界面) | 20-50 行 | 5-10 行 SQL |
| 部署复杂度 | 无 | 需安装 numpy/scipy | pip install duckdb 即可 |
| 结果可复现性 | 低 | 中 | 高(纯 SQL) |
七、变现建议:从技能到收入
掌握 DuckDB 模糊匹配去重这项技能后,有几个变现路径值得考虑:
1. 数据清洗 SaaS 服务
为中小型企业提供批量数据清洗服务。很多传统企业有数万条客户数据,需要去重、标准化、合并。你可以提供按次收费的服务(500-2000 元/次),或者开发成一个在线工具,用户上传 CSV,自动去重后下载。
2. 自动化数据管道搭建
很多公司在做数据仓库搭建时,第一道关卡就是数据去重。你可以将这套去重逻辑封装成通用的 ETL 模板,为企业搭建自动化数据管道收费(5000-30000 元/项目)。
3. 内容变现
将这套方法写成系列教程(比如「DuckDB 数据清洗七件套」),在知识星球、付费专栏或 Medium 上发布。一个 5000 字的深度教程可以卖 99-299 元,如果结合视频教程,价值可以翻 3 倍。
4. 企业内部培训
很多公司的数据团队还在用 Excel 手动去重。你可以提供 1-2 小时的内训课程,教他们用 DuckDB 自动化数据清洗流程。单次培训费用 3000-8000 元。
5. 开源项目商业化
基于去重 Pipeline 构建一个开源的 duckdb-dedup 工具包,然后通过提供商业支持、云部署服务或企业版功能来盈利。这是 Long Tail 策略,前期投入大,但长期回报稳定。
八、落地清单
- 评估数据规模:1000 条以内直接用方案一(标准化+精确去重);1000-10000 条用方案二(编辑距离);超过 10000 条考虑方案三(Jaccard + 预筛选)
- 确定阈值:先拿 100 条数据试跑,人工检查结果,微调阈值
- 输出结果:去重后的结果导出为 CSV/Excel,给业务方确认
- 建立规则库:把验证过的规则(正则表达式、阈值)固化下来,以后直接复用
记住一个原则:去重永远不是 100% 准确的。你的目标不是找到「绝对正确」的答案,而是找到「足够好」的答案,然后把剩下的交给人工审核。
DuckDB 帮你把「足够好」的速度提升到秒级,人工审核的量降低到几十条——这才是技术的真正价值。
💡 想系统学习 DuckDB 高性能数据分析技巧?duckdblab.org 上有完整的进阶教程系列,从模糊匹配到外部表到分布式查询,一步步带你掌握企业级数据应用。