导读:本期聚焦于唐僧创作的《MySQL中如何实现多字段分组去重?GROUP BY与DISTINCT组合使用详解》,敬请观看详情。数据表中出现重复记录是数据库操作里常见又头疼的问题,尤其在按多个字段判断唯一性时,单纯依赖一个DISTINCT关键字往往得不到想要的结果。本文围绕MySQL中的多字段去重展开,详细讲解GROUP BY与DISTINCT的区别和联系,分析它们分别适用于哪些场景,并给出多字段联合去重、保留最新一条记录、删除重复数据的完整SQL写法。文中还对比了不同去重方案的执行效率,介绍了索引对去重性能的影响,以及利用子查询、窗口函数ROW_NUMBER实现保留指定记录的技巧,帮助你根据实际数据量和业务需求选择最合适的去重方案。

在处理业务数据时,我们经常遇到这样的情况:一张表中因为程序bug、重复导入或者并发写入,产生了大量按某几个字段判断属于“重复”的记录。比如订单表里同一个用户对同一件商品下了多笔相同订单,日志表里同一条事件被记录了多次。这类问题用MySQL解决时,绕不开两个关键字:GROUP BY和DISTINCT。这篇文章就来详细讲讲这两个关键字的组合用法,以及多字段去重的各种实战写法。

MySQL中如何实现多字段分组去重?GROUP BY与DISTINCT组合使用详解

一、先弄清楚DISTINCT和GROUP BY的本质区别

很多初学者觉得DISTINCT和GROUP BY效果差不多,能去重就行,随便用哪个都可以。实际上两者虽然经常能返回相同的结果,但底层定位完全不同。DISTINCT的作用是把结果集中完全相同的行合并为一行,它关注的是“输出的行不重复”;而GROUP BY是把数据按指定字段分组,目的是对每一组做聚合运算,比如COUNT、SUM、MAX等。

看一个具体例子,假设有一张订单明细表order_detail,包含user_id、product_id、create_time三个字段。如果想查出所有不重复的用户和商品组合,下面两种写法结果完全一样:

-- 写法一:DISTINCT
SELECT DISTINCT user_id, product_id FROM order_detail;

-- 写法二:GROUP BY
SELECT user_id, product_id FROM order_detail GROUP BY user_id, product_id;

需要注意一点,DISTINCT是对SELECT后面列出的所有字段组合起来判断重复,而不是对某一个字段去重。这一点经常被误解,有人写了SELECT DISTINCT user_id, product_id FROM ...却以为只对user_id去重,结果发现数据行数没变化还以为语句失效了。实际上只要user_id和product_id的组合不同,这一行就会被保留。

两者的差异主要体现在:DISTINCT只能返回去重后的原始值,没法附带聚合统计;GROUP BY则可以在分组的基础上取聚合结果,比如统计每个组合出现的次数:

SELECT user_id, product_id, COUNT(*) AS cnt
FROM order_detail
GROUP BY user_id, product_id
HAVING cnt > 1;

上面这条语句顺便还能筛选出重复的组合,是排查重复数据时最常用的手段之一。另外从执行计划角度看,在MySQL较老版本(5.7之前)中,DISTINCT在部分场景下会借助临时表,而GROUP BY在某些查询里可以走松散索引扫描,性能会更好一些。不过MySQL 8.0之后优化器对两者的处理已经越来越接近,大多数情况下执行计划是一致的。

二、多字段组合去重的实战写法

多字段去重的核心思路是把所有参与判断的字段一起放进DISTINCT或者GROUP BY里。但在真实业务中,需求往往不止“查出去重结果”这么简单,更常见的是:查出每个分组中最新的一条记录。这种需求单靠DISTINCT就做不到了,需要借助子查询配合GROUP BY。

1. 基础多字段去重查询

SELECT DISTINCT user_id, product_id, order_status
FROM order_detail
WHERE create_time >= '2024-01-01';

这条语句会对user_id、product_id、order_status三个字段的组合进行去重,注意WHERE条件要写在DISTINCT前面,因为SQL的执行顺序是先过滤再去重,先缩小数据范围能明显减少去重的工作量。

2. 每组保留最新一条记录

这是多字段去重里最经典的需求。思路是先用GROUP BY找出每个组合的最大时间(或其他排序依据),再回表关联取出完整记录:

SELECT t.*
FROM order_detail t
INNER JOIN (
    SELECT user_id, product_id, MAX(create_time) AS max_time
    FROM order_detail
    GROUP BY user_id, product_id
) tmp ON t.user_id = tmp.user_id
     AND t.product_id = tmp.product_id
     AND t.create_time = tmp.max_time;

这种写法在MySQL 5.x版本中非常通用。但它有一个隐患:如果同一个组合里恰好有两条记录的create_time完全相同,那么这两条都会被查出来,去重就不彻底。遇到这种情况可以再取一个MAX(id)作为兜底判断条件,确保排序依据的绝对唯一性。

3. 使用窗口函数ROW_NUMBER

MySQL 8.0引入了窗口函数,处理这类需求优雅得多。ROW_NUMBER可以给每组内的记录编号,然后只取编号为1的那条:

SELECT *
FROM (
    SELECT t.*,
           ROW_NUMBER() OVER (
               PARTITION BY user_id, product_id
               ORDER BY create_time DESC, id DESC
           ) AS rn
    FROM order_detail t
) x
WHERE x.rn = 1;

PARTITION BY指定了分组的字段组合,ORDER BY决定组内保留哪一条。这里在排序末尾加上id DESC正是为了解决时间相同导致的并列问题。窗口函数的写法逻辑清晰,而且不用像子查询那样回表关联两次,在数据量大时通常表现更好。

三、物理删除表中的重复数据

查询去重只是第一步,很多场景下需要真正把重复数据从表里删掉。删除操作风险较高,动手前一定要备份,并且在事务中执行或者先用SELECT验证要删的数据范围。

1. 保留id最小的一条,删除其余重复记录

DELETE t FROM order_detail t
INNER JOIN order_detail t2
    ON t.user_id = t2.user_id
   AND t.product_id = t2.product_id
   AND t.id > t2.id;

这个自连接删除的思路是:让每一行去和同组中id更小的行比较,只要存在比自己id小的同组记录,说明自己不是最早的那条,就该被删除。写法简洁,但在没有合适索引的情况下,自连接的复杂度会很高,大表上执行可能非常慢。

2. 通过临时表方式处理大表去重

如果表数据量达到千万级,直接DELETE会产生大事务和大量行锁,更稳妥的做法是先把去重后的数据插入一张新表,再通过RENAME原子性地替换旧表:

-- 创建新表并插入去重后的数据
CREATE TABLE order_detail_new LIKE order_detail;

INSERT INTO order_detail_new
SELECT *
FROM (
    SELECT t.*,
           ROW_NUMBER() OVER (
               PARTITION BY user_id, product_id
               ORDER BY create_time DESC, id DESC
           ) AS rn
    FROM order_detail t
) x
WHERE x.rn = 1;

-- 验证数据无误后原子替换
RENAME TABLE order_detail TO order_detail_old,
             order_detail_new TO order_detail;

这种方式的好处是删除动作几乎瞬间完成,业务停顿时间极短,缺点是需要临时占用双倍存储空间,而且替换期间新写入的数据需要另行处理,适合可以在低峰期执行的批处理任务。

四、性能优化:索引对去重效率的影响

无论用GROUP BY还是DISTINCT,去重本质上都需要对数据排序或哈希分组。如果参与分组的字段上有合适的联合索引,MySQL可以直接利用索引的有序性完成分组,避免额外的排序和临时表,性能差距可能是几十倍。

针对前面例子中的表,建立联合索引时有讲究:

-- 联合索引的字段顺序要和GROUP BY的字段顺序一致
ALTER TABLE order_detail
ADD INDEX idx_user_product (user_id, product_id, create_time);

把create_time也放进索引有两个好处:一是覆盖索引让分组统计不用回表,二是“每组取最新一条”的子查询里MAX(create_time)可以直接从索引中读取。可以用EXPLAIN验证,当Extra列出现Using index for group-by时,说明MySQL正在利用松散索引扫描优化分组操作,这是GROUP BY最理想的执行方式。

还有几个实践建议值得参考:去重前先用WHERE尽量过滤无关数据,不要把几十万行数据全捞出来再去重;对经常出现重复的表,考虑在业务层加唯一索引从源头杜绝重复写入,比如对user_id和product_id建UNIQUE KEY,插入时用INSERT IGNORE或者ON DUPLICATE KEY UPDATE兜底,这比事后清理要省心得多;删除重复数据时务必分批执行,每批控制在一两千行左右,避免长事务阻塞其他查询。

总结一下,简单的结果去重用DISTINCT足够,涉及分组统计用GROUP BY,要保留组内特定记录就上窗口函数ROW_NUMBER,物理清理大表则推荐临时表替换方案。根据数据量和业务容忍度选对工具,多字段去重并不难处理。

MySQL去重GROUP BYDISTINCT修改时间:2026-09-09 06:06:40

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260909/53238.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。