如何使用 SQL SELECT 查询重复数据?

沈知远
沈知远
发布时间:2026-08-25| 140人浏览过
原创

前言

查 SQL 重复数据,最容易混淆的是“找出重复值”和“定位重复整行”其实不是同一个问题。本文按查询目标和 MySQL 版本分别拆解写法,再补上删除前必须确认的保留规则,帮助你根据数据库能力、结果粒度和误删风险选到合适方案。

查重复数据时,很多人第一反应是加上 DISTINCT,但这只能去重,不能告诉你“哪些值重复了、哪几行需要处理”。真正要把结果查准,必须先分清目标:你是只想知道重复值,还是要把每一条重复记录连同原始 ID 一起找出来。

下面按这两个问题展开,再补充 MySQL 版本差异和删除前的确认要点。看完后,你可以根据是否支持窗口函数、是否需要保留首条记录,快速判断该用哪种写法。

先找出哪些值发生了重复

如果你的目标只是确认“哪些邮箱重复了”,最直接的写法就是 GROUP BY 配合 HAVING。这类查询适合先看数据分布,确认重复范围,再决定后续是否清理。

用分组统计定位重复值的专栏式示意图
先定位重复值,不急着查整行先分组统计,再用 HAVING 过滤,是定位重复值的基础写法;它适合看重复范围。

假设表 users 中要检查 email 是否重复:

SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

这条语句的关键点有三个:

  • GROUP BY email 会把相同邮箱归到同一组。
  • HAVING COUNT(*) > 1 用来过滤分组后的结果,只保留出现次数大于 1 的邮箱。
  • COUNT(*) 属于聚合结果,不能写进 WHERE,否则会报错 Invalid use of aggregate function。

这里也能看出 DISTINCT 和“查重复”不是一回事。DISTINCT 只负责把结果集中的重复值压成一条,不会把重复次数告诉你,也不能直接定位哪些记录重复。

如果你要检查的是“整行是否重复”,而不是单个字段重复,那么单用这个思路会更麻烦。因为 GROUP BY 后通常需要把参与判断的字段全部列出来;字段一多,查询可读性和维护性都会变差。这种场景通常更适合放到下一节,用窗口函数或其他方式处理。

需要把重复记录整行查出来时怎么写

实际业务里,更常见的问题不是“哪个邮箱重复”,而是“重复的是哪几条数据”。这时只返回 email 和次数还不够,往往还要把 id、name 等原始字段一起取出来,方便排查或后续删除。

如果数据库支持窗口函数,推荐直接使用 ROW_NUMBER()。适用范围包括 MySQL 8.0+、PostgreSQL、SQL Server:

SELECT id, email, name
FROM (
SELECT id, email, name,
 ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
) t
WHERE rn > 1;

这条写法的思路是先给每个重复组内部编号,再过滤掉每组中的第一条:

  • PARTITION BY email 表示按邮箱分组,每个邮箱各自编号。
  • ORDER BY id 决定了组内的先后顺序。
  • ROW_NUMBER() 会让每组第一条记录编号为 1,后续记录依次变成 2、3……
  • 外层用 WHERE rn > 1,就能只取出“非首次出现”的重复行。

这个方案的优势在于,它不仅能查出重复行,还顺带表达了“保留谁、处理谁”的规则。比如上面的 ORDER BY id,默认就是保留较早的那条记录。如果你后面要做清理,这个排序条件就是判断依据的一部分。

有些场景也会改用 COUNT(*) OVER (PARTITION BY email)。这样每一行都能看到自己所在分组的总条数,也能判断是否重复。不过如果你最终仍然要排除“首条保留、其余待处理”的记录,ROW_NUMBER() 一般更直接,筛选逻辑也更清楚。

MySQL 5.7 及更老版本怎么办

如果你用的是 MySQL 5.7 或更早版本,就不能使用窗口函数,也没有 CTE(WITH 子句)可用。此时常见做法是自连接,让同一张表中的重复值互相匹配。

MySQL 不同版本查重复整行方案对比图
整行查重:窗口函数和自连接怎么选要拿到完整重复记录,支持窗口函数的版本更直接;老版本则通常退回到自连接,并额外关注。

一个比较常见的写法如下:

SELECT DISTINCT u1.*
FROM users u1
INNER JOIN users u2
ON u1.email = u2.email AND u1.id != u2.id;

它的核心逻辑是:只要某条记录能在表里找到另一条 email 相同、但 id 不同的记录,就说明它属于重复数据。

  • u1.email = u2.email 用来匹配重复值。
  • u1.id != u2.id 必须保留,否则每条记录都会和自己匹配,结果就失真了。
  • DISTINCT 也不能省,因为一组内如果有多条重复记录,连接后会产生多组配对结果。

这一点很容易被低估。比如同一个邮箱出现 3 次,连接结果会产生多对匹配关系,而不是简单返回 3 行。所以最终需要 DISTINCT 去掉这些重复返回的组合。

这个方案能用,但在大表上要格外注意性能。没有合适索引时,自连接很容易把查询拖慢,甚至接近 O(n²) 扫描。至少要给 email 建好索引,这样数据库在匹配同值记录时才有机会走得更稳。

准备删除前,先把保留规则说清楚

查到重复数据只是第一步,真正容易出错的地方往往发生在删除之前。很多问题不是 SQL 写不出来,而是“到底保留哪一条”没有提前定义清楚。

删除重复数据前确认保留策略的编辑插画
删除前先确认:保留谁、怎么复核删除前先定义保留规则,再用 SELECT 复核结果,是避免误删的关键步骤。

如果你使用的是窗口函数方案,保留策略就体现在排序条件里。例如:

  • 写成 ORDER BY id,通常表示保留 ID 更小、出现更早的记录。
  • 如果业务要求保留最新数据,就应该改成 ORDER BY created_at DESC,让最新记录成为第 1 条。

这一步不要凭习惯写。因为排序条件一变,rn = 1 对应的“保留记录”也会跟着变,后面的删除范围自然全变了。

在老 MySQL 中,删除通常比查询更绕。常见做法是先把需要保留的 ID 存到临时表,再执行类似 DELETE WHERE id NOT IN (...) 的语句。但这里还有一个典型坑:NOT IN 一旦遇到 NULL,判断结果可能失效,所以必须额外确认数据里是否存在 NULL,或者改用更稳妥的写法。

无论使用哪种数据库、哪套删除方案,正式执行 DELETE 前都应该先把同样的条件跑成 SELECT 再复核一遍。尤其是在字段组合较多、历史脏数据复杂、空值混杂时,这一步能拦下很多本来可以避免的误删。

重复数据看起来是简单问题,但真正影响结果的因素并不少:字段怎么组合、按什么顺序保留、数据库版本支不支持窗口函数、查询里是否存在 NULL 干扰,这些都会直接改变最终结果。最稳妥的做法,是先用 SELECT 观察实际数据分布,再选匹配当前版本和业务规则的方案,而不是直接套一段固定模板。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。