在DB2中清理重复数据,难点通常不在查询,而在删除。用SELECT DISTINCT可以很快看到唯一值,但它不会释放表空间,也不会减少后续全表扫描的数据量。重复行可能来自上游系统重复下发、历史数据导入未加约束,或者主键设计只覆盖了部分列。物理删除前,必须先用业务规则把重复组圈出来,并保证每组只删多余记录。下面从重复键定义、窗口函数定位、临时表重建和批量事务控制几个角度展开。

一、去重前先定义重复键并做好备份
重复键的定义不能凭感觉。假设有一张员工联系方式表employee_contacts,字段包含emp_id、phone、email和update_time。如果业务规定同一员工同一电话同一邮箱只保留一条,那么重复键就是emp_id + phone + email。把update_time也纳入重复判断是不合适的,因为更新时间不同会导致完全相同行几乎不存在,反而漏掉真正的重复数据。
在没有主键或唯一约束保护时,这种重复很容易产生。比如上游系统每天全量同步一次,DB2表只做追加插入,时间一长就会出现大量逻辑相同但时间字段不同的记录。开始删除前,建议先为原始表做一次快照,可以使用CREATE TABLE ... AS (... ) WITH DATA把数据复制到备份表,也可以使用db2 export导出为IXF文件。删除动作一旦提交,恢复成本很高,尤其是没有备份时。
另外要检查依赖对象。如果这张表被外键引用,直接删除父表多个行时可能报外键冲突;如果表上有删除触发器,还必须确认触发器逻辑是否会记录日志、更新统计表或调用外部程序。去重本质上是批量删除,必须把这些副作用提前识别清楚。
二、用ROW_NUMBER窗口函数定位重复行
DB2支持ROW_NUMBER()窗口函数,非常适合在重复组内编号。先创建一张示例表并插入两对重复数据:
CREATE TABLE employee_contacts (
emp_id INT NOT NULL,
phone VARCHAR(20),
email VARCHAR(100),
update_time TIMESTAMP
);
INSERT INTO employee_contacts VALUES
(1001, '13800000000', 'a@ipipp.com', TIMESTAMP('2024-01-01 10:00:00')),
(1001, '13800000000', 'a@ipipp.com', TIMESTAMP('2024-01-02 10:00:00')),
(1002, '13900000000', 'b@ipipp.com', TIMESTAMP('2024-01-03 10:00:00')),
(1002, '13900000000', 'b@ipipp.com', TIMESTAMP('2024-01-03 12:00:00'));
去重时需要指定PARTITION BY和ORDER BY。PARTITION BY后面放重复键,ORDER BY决定保留哪一条,比如按update_time DESC保留最新时间,时间相同再按emp_id兜底。查询每个重复组内的编号可以这样写:
SELECT
emp_id,
phone,
email,
update_time,
ROW_NUMBER() OVER (
PARTITION BY emp_id, phone, email
ORDER BY update_time DESC, emp_id
) AS rn
FROM employee_contacts;
执行后,重复组内最新记录rn为1,其余记录rn大于1。只要删除rn > 1的行即可。不过DB2中窗口函数不能直接出现在WHERE子句中,所以不能简单拼接WHERE ROW_NUMBER() > 1。需要把上面的查询包成子查询或派生表,再做删除或物化。
三、通过临时表重建实现低风险删除
如果表中没有可靠的主键,直接删除重复行时很难精确定位到某一条物理记录。例如重复组内除了时间不同,其他业务列完全一样,用DELETE ... WHERE emp_id=... AND phone=...会把整组都删掉。更稳妥的办法是先物化保留行到临时表,再替换原表。
下面的语句先创建一个结构相同的新表,并把rn = 1的唯一记录插入进去:
CREATE TABLE employee_contacts_clean LIKE employee_contacts;
INSERT INTO employee_contacts_clean (emp_id, phone, email, update_time)
SELECT emp_id, phone, email, update_time
FROM (
SELECT
t.*,
ROW_NUMBER() OVER (
PARTITION BY emp_id, phone, email
ORDER BY update_time DESC, emp_id
) AS rn
FROM employee_contacts t
) s
WHERE s.rn = 1;
此时可以先用SELECT COUNT(*)对比原表行数和清洗表行数,确认删除数量符合预期。原表有4行,清洗表应有2行。如果确认无误,下一步可以清空原表并把数据导回:
TRUNCATE TABLE employee_contacts IMMEDIATE; INSERT INTO employee_contacts (emp_id, phone, email, update_time) SELECT emp_id, phone, email, update_time FROM employee_contacts_clean;
使用TRUNCATE的好处是速度快、事务日志量小,但TRUNCATE会删除所有行,并且可能受到外键或权限限制。如果原表不能被清空,也可以使用RENAME TABLE交换表名,但重命名会短暂影响依赖对象。操作完成后记得分析表统计信息,RUNSTATS可以帮助优化器获得准确的行数和索引分布。
如果原表存在唯一主键或自增列,删除语句可以更直接。保留每组中最小id的记录,删除其余行:
DELETE FROM employee_contacts e
WHERE EXISTS (
SELECT 1
FROM employee_contacts keep
WHERE keep.emp_id = e.emp_id
AND keep.phone = e.phone
AND keep.email = e.email
AND keep.id < e.id
);
注意EXISTS里的条件keep.id < e.id表示:当前行如果能找到同组且id更小的行,说明它不是最小id,可以删除;最小id的行因找不到更小id而保留。如果删除条件写成NOT IN,一旦子查询结果包含NULL,就会出现空结果,导致一条都删不掉,这是去重时经常踩的坑。
四、大数据量去重的性能与事务控制
当表达到千万行级别时,去重操作不能只靠一条DELETE完成。删除会记录事务日志,如果事务过大,可能触发日志空间不足,DB2会中断事务并回滚,浪费大量时间。建议把删除过程拆成多个小事务,比如按日期、按编号区间或按主键分片。临时表重建方式虽然要写一遍全表,但INSERT ... SELECT配合合适的索引,常常比逐行删除更快。
索引设计对去重视图影响明显。为(emp_id, phone, email, update_time)建立复合索引,可以让窗口函数的分区排序尽量走索引,减少内存排序和临时表溢出。如果没有索引,DB2需要对所有重复键做哈希或排序,数据量一大就容易出现SORTHEAP不足或临时表空间满。去重前可以先执行RUNSTATS,让优化器选择更合理的计划。
还需要考虑并发行操作。如果业务允许维护窗口,去重期间可以停止写入,避免出现“边删边插入新重复”的情况。如果不允许停机,可以在应用层先加唯一约束,或使用DB2的在线表重组功能配合清理。删除完成后,建议立刻补上唯一索引或约束,从根上防止重复数据再次出现。比如在employee_contacts表上创建复合唯一索引emp_id + phone + email,下一步插入重复值就会被DB2直接拒绝。
最后再执行一次统计信息收集和表重组:
RUNSTATS ON TABLE employee_contacts
WITH DISTRIBUTION AND DETAILED INDEXES ALL;
REORG TABLE employee_contacts;
重组可以回收删除行留下的空洞,使数据页更紧凑,减少扫描开销。完成这些步骤后,DB2表既完成了逻辑去重,也完成了物理空间整理。相比只查DISTINCT的临时方案,这种清理方式才能真正改善表的长期访问性能。
DB2去重重复数据删除ROW_NUMBER修改时间:2026-10-04 06:18:56