分组查询本身不处理重复数据,它只按指定字段归组并聚合;真正影响结果的是你如何定义“重复”、是否过滤、以及是否需要原始行——选错方法会导致漏数据、报错或删错。

GROUP BY 后没写 HAVING 就等于没查重复
只写 SELECT email, COUNT(*) FROM users GROUP BY email 会把所有邮箱都列出来,包括只出现一次的。业务上要的“重复”,必须靠 HAVING COUNT(*) > 1 过滤分组后的结果。WHERE 在分组前执行,没法用 COUNT(*),写了也无效。
- MySQL 5.7+ 和 PostgreSQL 严格模式下,
SELECT *配GROUP BY email直接报错,因为非分组字段(如id)无法确定取哪一行 -
HAVING COUNT(email) > 1和HAVING COUNT(*) > 1效果不同:前者忽略email IS NULL的行,后者把 NULL 当作一个组统计 - 别名(如
COUNT(*) AS cnt)不能在HAVING中直接用:HAVING cnt > 1多数数据库不支持,得重复写COUNT(*)
想看重复数据的完整行,别硬套 GROUP BY
GROUP BY 只返回分组摘要,拿不到原始记录。要查出所有重复的完整行,得用子查询或窗口函数。
- 用
IN子查询最兼容:SELECT * FROM users WHERE email IN (SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1),但注意IN会跳过NULL,需额外加OR email IS NULL判断 - 大表慎用
IN,性能差;改用EXISTS或给email加索引 - MySQL 8.0+ / PostgreSQL 推荐窗口函数:
SELECT *, COUNT(*) OVER (PARTITION BY email) AS dup_cnt FROM users WHERE dup_cnt > 1,一行搞定,且保留所有字段
删除重复行时,GROUP BY 不能直接 DELETE
没有 DELETE FROM t GROUP BY x 这种语法。真要删,必须明确“保留哪一条”,再用主键或唯一标识定位。
- 安全做法是留最小
id:DELETE t1 FROM users t1 INNER JOIN users t2 WHERE t1.email = t2.email AND t1.id > t2.id - SQLite 不支持
DELETE ... JOIN,得用ROWID子查询:DELETE FROM users WHERE ROWID NOT IN (SELECT MIN(ROWID) FROM users GROUP BY email) - 用 CTE +
ROW_NUMBER()更可控:WITH ranked AS (SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users) DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn > 1)
最容易被忽略的是 NULL 值行为:所有 NULL 在 GROUP BY 中被当作同一组,在 DISTINCT 中也被合并,但业务上可能要求区分。处理前先确认 NULL 是否算“重复”,再决定用 COALESCE(email, 'unknown_' || id) 还是接受默认行为。

















