最直接方法是group by+having count(*)>1;仅返回重复值摘要,需完整行时优先用窗口函数row_number()或inner join,避免in子查询因null失效。

用 GROUP BY + HAVING 查重复值最直接
查重复记录,优先用 GROUP BY 配合 HAVING COUNT(*) > 1,而不是嵌套子查询——它更易读、性能更好、兼容所有主流数据库。
常见错误是写成 SELECT * FROM table WHERE id IN (SELECT id FROM table GROUP BY id HAVING COUNT(*) > 1),这看似“用了子查询”,但实际多此一举,且可能因 NULL 或数据类型不匹配出错。
- 只对要判断重复的字段做
GROUP BY,比如查邮箱重复就GROUP BY email -
HAVING必须跟在GROUP BY后,不能写成WHERE COUNT(*) > 1(语法错误) - 若需返回完整行,用
INNER JOIN关联原表,而非IN(避免 NULL 导致结果丢失)
需要原始行数据?用窗口函数替代子查询
想列出所有重复的完整记录(不止重复值本身),ROW_NUMBER() 窗口函数比相关子查询更可靠、更高效。
典型错误是写 SELECT * FROM t1 WHERE (col1, col2) IN (SELECT col1, col2 FROM t1 GROUP BY col1, col2 HAVING COUNT(*) > 1),在 MySQL 5.7 或 SQL Server 上可能报错或漏数据。
- PostgreSQL / SQL Server / MySQL 8.0+ 支持:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users) t WHERE t.rn > 1
- MySQL 5.7 不支持窗口函数,只能退回到
JOIN方式,别硬套子查询 -
PARTITION BY字段必须和你要判定重复的列完全一致,顺序无关但数量不能少
子查询真要用?注意 correlated 子查询的性能陷阱
只有在极少数逻辑必须逐行判断时(如“找每个重复组里创建时间最早的那条”),才考虑相关子查询,但它极易拖慢查询。
例如 SELECT * FROM users u1 WHERE EXISTS (SELECT 1 FROM users u2 WHERE u2.email = u1.email AND u2.id 看似简洁,但数据量一过万,执行计划常变成嵌套循环,全表扫描次数爆炸。
- 务必给关联字段加索引,比如
CREATE INDEX idx_email ON users(email) - 避免在子查询里用
SELECT *或聚合函数,只返回常量(如SELECT 1) - 用
EXISTS代替IN,特别是当子查询可能返回 NULL 时,IN会整个失效
去重后保留一条?别依赖子查询删数据
用子查询 DELETE 删除重复行风险很高,容易误删或锁表太久。生产环境应避免 DELETE FROM t WHERE id NOT IN (SELECT MIN(id) FROM t GROUP BY col) 这类写法。
MySQL 报错 “You can’t specify target table for update in FROM clause”,SQL Server 可能锁住整个表,PostgreSQL 要加 ctid 才安全。
- 安全做法:先用
CREATE TABLE dedup AS SELECT DISTINCT ON (email) * FROM users ORDER BY email, created_at DESC(PostgreSQL) - MySQL 8.0+ 推荐用
ROW_NUMBER()写 CTE 再删:WITH ranked AS (SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) rn FROM users) DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn > 1)
- 任何删除操作前,先
SELECT验证子查询结果,尤其注意NULL值是否被当作相同重复项
重复数据逻辑看着简单,但字段含 NULL、字符集差异、索引缺失这几个点,会让子查询行为突然偏离预期。动手前先 SELECT COUNT(*), COUNT(email), COUNT(DISTINCT email) FROM table 看看 NULL 占比。











