重复数据分类:完全重复与部分重复,精确匹配与模糊匹配,单字段与多字段重复
做数据去重,第一件事不是写代码,而是搞清楚你面对的是哪种「重复」。
我见过不少团队,上来就怼一个 DISTINCT 或者 GROUP BY,结果数据一跑,该去的没去掉,不该删的删了一大片。说白了,重复数据不是铁板一块,它有好几种面孔。你认不出它,它就坑你。
这一节,咱们就把重复数据的分类掰开揉碎讲清楚。我按三个维度来切:重复程度、匹配方式、字段范围。你把这套分类体系吃透了,后面写去重逻辑的时候,心里就有谱了。
核心观点:重复数据分类是去重策略的「地图」。没有这张地图,你就是在盲人摸象。
一、按重复程度:完全重复 vs 部分重复
这是最直观的分类方式。我刚开始做数据清洗的时候,以为重复就是「两行数据一模一样」。后来发现,事情远没那么简单。
1. 完全重复(Exact Duplicate)
两行记录的所有字段值都完全相同。这种重复最常见,也最好处理。
举个例子:
| user_id | name | phone | |
|---|---|---|---|
| 1001 | 张三 | zhangsan@example.com | 13800138001 |
| 1001 | 张三 | zhangsan@example.com | 13800138001 |
这两行完全一样。原因可能是上游系统重复推送、接口重试、或者人为误操作。处理方式很简单:DISTINCT 或者 ROW_NUMBER() 去重就行。
我的经验:完全重复往往不是业务逻辑问题,而是数据管道的问题。我曾经在一个日志系统里发现,某台服务器因为网络抖动,把同一批数据发了三遍。去重只是治标,找到根因才是治本。
2. 部分重复(Partial Duplicate)
两行记录只有部分关键字段相同,其他字段可能不同。这才是真正考验功底的地方。
举个例子:
| order_id | user_id | product | amount | create_time |
|---|---|---|---|---|
| ORD001 | 1001 | 手机 | 2999 | 2024-01-01 10:00:00 |
| ORD001 | 1001 | 手机 | 2999 | 2024-01-01 10:00:05 |
你看,order_id、user_id、product、amount 都一样,但 create_time 差了 5 秒。这算不算重复?
算。因为业务上,同一个订单不可能在 5 秒内下两次。这多半是系统并发处理时产生的时间戳偏差。
注意:部分重复的判断标准取决于业务规则。同一个用户同一天下了两笔相同金额的订单,在电商系统里可能是正常行为,但在风控系统里可能就是重复。一定要跟业务方确认「重复」的定义。
二、按匹配方式:精确匹配 vs 模糊匹配
这个维度决定了你怎么「找」重复。说白了,就是比较两个值的时候,用等号还是用相似度。
1. 精确匹配(Exact Match)
值必须完全相等。比如 '张三' == '张三',1001 == 1001。这种匹配效率高,适合主键、ID、标准化后的字段。
我建议,能用精确匹配的地方,绝不用模糊匹配。为什么?因为精确匹配可以用哈希、索引、排序,性能好得多。
-- 精确匹配去重示例(SQL)
SELECT DISTINCT user_id, email
FROM users;
-- 或者用窗口函数保留第一条
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS rn
FROM users
)
SELECT * FROM ranked WHERE rn = 1;
2. 模糊匹配(Fuzzy Match)
值不完全相等,但「看起来很像」。比如 '张 三' 和 '张三',138-0013-8001 和 13800138001。
模糊匹配常用的方法有:
- 编辑距离(Levenshtein Distance):计算两个字符串需要多少次增删改才能变成一样
- Jaccard 相似度:计算两个集合的交集与并集之比
- Soundex / Metaphone:按发音匹配,适合人名
- 正则表达式归一化:先清洗再精确匹配
避坑指南:我曾经在一个客户数据合并项目里,直接用编辑距离匹配姓名,结果把「王芳」和「王方」这种真·不同的人给合并了。后来加了「地区+生日」作为辅助判断,才把误杀率降下来。模糊匹配一定要设阈值,而且阈值要经过测试。
三、按字段范围:单字段 vs 多字段
这个维度决定了「重复」的判断依据是什么。是一个字段说了算,还是多个字段一起说了算。
1. 单字段重复(Single-Field Duplicate)
只根据一个字段的值来判断是否重复。比如「邮箱不能重复」、「身份证号不能重复」。
这种场景最简单,直接 GROUP BY 或者建唯一索引就行。
-- 单字段重复检测
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
2. 多字段重复(Multi-Field Duplicate)
需要多个字段组合起来判断。比如「同一个用户在同一天不能下两笔相同金额的订单」。
多字段重复又分两种:
- 组合键重复:多个字段的值组合起来必须唯一。比如
(user_id, order_date, amount) - 业务规则重复:字段之间有关系,比如「地址相似 + 姓名相似 + 电话相同」才算重复
-- 多字段组合重复检测
SELECT user_id, DATE(create_time), amount, COUNT(*)
FROM orders
GROUP BY user_id, DATE(create_time), amount
HAVING COUNT(*) > 1;
我的习惯:多字段去重时,我会先建一个「去重指纹」字段。比如把 user_id、date、amount 拼接起来,做一次 MD5。这样后续判断就变成了单字段精确匹配,性能提升很明显。
四、分类组合:实际场景中的交叉应用
这三个维度不是孤立的。实际项目中,它们经常组合出现。我整理了一张对照表,方便你快速定位:
| 场景 | 重复程度 | 匹配方式 | 字段范围 | 典型处理 |
|---|---|---|---|---|
| ETL 全量快照 | 完全重复 | 精确匹配 | 全字段 | DISTINCT / 去重表 |
| 用户注册 | 部分重复 | 精确匹配 | 单字段(邮箱) | 唯一索引 |
| 客户数据合并 | 部分重复 | 模糊匹配 | 多字段(姓名+电话+地址) | 相似度计算 + 人工审核 |
| 订单去重 | 部分重复 | 精确匹配 | 多字段(订单号+金额) | 窗口函数 + 时间窗口 |
| 日志去重 | 完全重复 | 精确匹配 | 全字段 | Bloom Filter / 哈希 |
你看,同一个场景可能同时涉及多个分类维度。比如「客户数据合并」就是「部分重复 + 模糊匹配 + 多字段」的组合,这种最复杂,也最容易踩坑。
再次提醒:分类不是目的,目的是选对去重策略。你花 10 分钟把重复数据的「画像」画清楚,后面写代码可能只需要 5 分钟。反过来,上来就写代码,调试可能要花 2 小时。