在数据库日常查询中,我们经常会遇到这样的场景:一张用户行为表或者订单表中,由于程序 bug、重试机制或者人工导入,出现了大量内容重复的行。所谓重复值,通常是指某些列的组合完全一致的多条记录。如果直接对这些数据进行汇总,统计结果就会被放大,影响报表准确性。要解决这类问题,核心思路是利用 SQL 的分组能力,把相同的组合归为一组,再计算每组出现的次数。

一、使用 GROUP BY 与 COUNT 找出重复组
最基础的做法是,针对你认为可能重复的列使用 GROUP BY,然后用 COUNT 聚合函数统计每组的数量。比如我们有一张 order_log 表,包含 user_id、amount 和 create_date 字段,想看哪些用户在哪一天产生了相同金额的重复下单,就可以按这三列分组。
这里要注意,COUNT(*) 会统计组内所有行,包括空值行;而 COUNT(列名) 只统计该列非空的行。如果重复判断依赖的字段可能出现 NULL,两者结果会有偏差。一般找重复值用 COUNT(*) 即可,因为它关心的是行数本身。
SELECT
user_id,
amount,
create_date,
COUNT(*) AS repeat_count
FROM order_log
GROUP BY user_id, amount, create_date
HAVING COUNT(*) > 1;
上面的语句中,HAVING 子句非常关键。WHERE 是在分组前过滤单行,而 HAVING 是在分组后过滤组。只有重复次数大于 1 的组才会被保留,这样就能精准列出所有重复组合及其出现次数。
这种写法的优点是直观、执行计划简单,大多数关系型数据库都能很好地优化。缺点在于它只给出了重复组的摘要,没有返回原始行的主键,如果后续要删除或标记明细,还需要再做一次关联查询。
二、关联原表定位重复明细
当我们从第一步拿到了重复组合后,往往还需要知道这些重复记录对应的具体 ID,以便排查或清理。此时可以把上面的查询作为子查询,通过相同的分组列关联回原表。
下面示例使用窗口函数也是另一种思路,但为保持兼容低版本数据库,这里展示 GROUP BY 关联写法。我们将重复组合存入临时结果,再 JOIN 原表拿出全部字段。
SELECT
o.id,
o.user_id,
o.amount,
o.create_date
FROM order_log o
INNER JOIN (
SELECT
user_id,
amount,
create_date
FROM order_log
GROUP BY user_id, amount, create_date
HAVING COUNT(*) > 1
) dup
ON o.user_id = dup.user_id
AND o.amount = dup.amount
AND o.create_date = dup.create_date
ORDER BY o.user_id, o.amount, o.create_date, o.id;
这样一来,就能看到每一条属于重复组的原始记录。如果表上有唯一索引或者业务主键,也可以在此基础上决定保留最小 ID 的行,其余做软删除。
需要注意的是,关联字段如果存在 NULL,等值连接会失败,因为 NULL 不等于 NULL。若业务允许,建议先使用 COALESCE 将 NULL 转为特定占位符再关联,或者在子查询中排除全 NULL 组合。
三、仅统计去重后的重复影响
有时我们并不关心重复明细,只想知道“有多少不重复的用户发生过重复行为”,或者“去重后真实订单总额是多少”。这时候 COUNT(DISTINCT 列) 比单纯 GROUP BY 更高效。
例如,想统计每个用户去重后的下单天数,同时看出其重复订单数,可以分开计算。DISTINCT 会忽略组内重复,而普通 COUNT 暴露重复,两者结合能全面评估数据质量。
SELECT
user_id,
COUNT(*) AS total_rows,
COUNT(DISTINCT amount, create_date) AS distinct_combo,
COUNT(*) - COUNT(DISTINCT amount, create_date) AS redundant_rows
FROM order_log
GROUP BY user_id;
该查询对每个用户输出总行数、不重复组合数和冗余行数。冗余行数大于 0 即代表存在重复。这种方式适合做数据质量巡检报表,不需要返回明细,性能也优于先查重复再关联。
不过 DISTINCT 在多列时会构造临时哈希表,数据量极大时可能吃内存。此时可优先考虑在应用层分批处理,或者只对核心字段建联合索引来加速分组。
四、避免常见误区
一个典型误区是认为 GROUP BY 之后可以直接在 SELECT 写任意列。标准 SQL 规定,SELECT 中非聚合列必须全部出现在 GROUP BY 中,否则结果不确定。有些数据库如 MySQL 在非严格模式下允许偷懒,但那会导致随机取值,排查重复时极易误判。
另一个误区是用 DELETE 先按 LIMIT 删重复,这种做法在并发环境会误删正常数据。正确做法是先通过 HAVING 确认重复范围,再用带有主键条件的语句精确清理,或者采用中间表替换法保证原子性。
-- 保留每组的第一个 id,删除其他重复行
DELETE FROM order_log
WHERE id NOT IN (
SELECT MIN(id)
FROM order_log
GROUP BY user_id, amount, create_date
);
上面的语句在多数库中可行,但若子查询返回 NULL 会导致 NOT IN 失效,稳妥写法是改用 NOT EXISTS 关联。无论哪种,执行前务必用 SELECT 预览受影响主键,确认无误再删。
总结来说,SQL 分组统计重复值的本质是利用 GROUP BY 归组、COUNT 计数、HAVING 过滤,再视需求关联或去重。理清字段空值、NULL 连接和删除安全边界,就能在报表与清洗中稳稳控住数据质量。