许多拥有多年经验的SQL开发者,在面试时遇到这个问题也常常会陷入思考:进行数据去重时,DISTINCT和GROUP BY应该如何选择?虽然两者看似都能实现去重功能,但其底层语义和执行逻辑截然不同。若使用不当,轻则引发SQL语法错误,重则导致查询结果出现偏差而难以察觉。本文将深入对比分析,帮你彻底理清DISTINCT与GROUP BY的核心区别与最佳实践。

需求一:仅需罗列“不重复值”——DISTINCT实现单列与多列去重
当业务场景仅需获取部门列表、城市名称或状态码等不重复值时,DISTINCT关键字是最直接、最高效的选择。它的核心使命就是返回“不重复的行”,避免了复杂的统计逻辑。
一个常见的误用场景是:强行使用GROUP BY代替DISTINCT进行简单去重。在MySQL 8.0及以上版本默认开启的only_full_group_by模式下,执行SELECT dept, salary FROM emp GROUP BY dept会直接报错;即使关闭该模式,查询返回的salary值也是随机且不可信的。
掌握DISTINCT的以下几个关键特性,能让你更得心应手:
- 它作用于整行记录,而非单一列。只有当所有被选中的字段值完全相同时,才判定为重复行。
- 多列组合去重是其核心优势,例如
SELECT DISTINCT user_id, order_date FROM orders,精准表达“同一用户同一订单日仅保留一条”的语义。 NULL值在去重时被视为一个独立的值,多个NULL行只会保留一个。- 它不能直接与聚合函数混用,类似
SELECT DISTINCT COUNT(*)的语法是无效的。
需求二:既要去重又要统计——GROUP BY配合聚合函数
当查询需求中包含“每个分类下的数量”、“平均销售额”、“最高温度”等分组统计指标时,这已不再是纯粹的去重问题,而是典型的分组聚合查询。此时,GROUP BY是唯一正确的语法选择。
之前提到的错误,本质上是试图用分组来实现去重,却未对非分组字段进行聚合包裹。正确的SQL写法是:SELECT dept, MAX(salary) FROM emp GROUP BY dept。必须牢记:出现在SELECT列表中的非分组字段,必须包含在聚合函数中。
在多字段分组场景下,GROUP BY dept, role的含义是“对部门与角色的组合进行分组”,而非分别对部门和角色去重。若需先对组合去重再进行统计,则应使用子查询嵌套:内层用DISTINCT生成唯一组合,外层进行GROUP BY聚合。
性能对比:大数据量下DISTINCT与GROUP BY的执行计划差异
当数据表记录超过百万级时,DISTINCT和GROUP BY的执行计划可能天差地别。特别是在处理多列组合或未建立索引的字段时,性能差异会急剧放大。
- 单列去重:通常
DISTINCT性能更优,数据库对其有专门的哈希算法优化,以快速过滤重复值。 - 多列组合去重(例如
user_id, product_id):在某些数据库引擎中,GROUP BY的表现更为稳定,因为它天然利用排序分组路径。 - 在无索引的字段上使用
DISTINCT,极易触发临时表与文件排序操作,相比GROUP BY更消耗内存资源。 - 无论使用哪种方式,在执行前务必通过
EXPLAIN命令检查是否有效利用了索引,切勿仅凭语句的简洁性来判断性能。
进阶难题:SQL去重后如何保留最新一条记录?
业务方提出“按账单号去重,并保留最新金额”的需求时,一个典型的SQL难题就出现了。DISTINCT无法控制保留哪一条记录,而GROUP BY的随机取值行为也不可靠。要实现“单字段去重 + 保留指定行”,必须借助窗口函数或优化的子查询。
- MySQL 8.0及以上版本:使用
ROW_NUMBER() OVER (PARTITION BY bill_no ORDER BY create_time DESC)窗口函数,对每组账单按时间倒序编号,从而筛选出最新记录。 - MySQL 5.7等旧版本:通过
LEFT JOIN或WHERE NOT EXISTS子查询,逻辑上找出“不存在更新时间比当前记录更大的同账单号”的行,从而确保获取的是最新记录。 - 特别提醒:强行使用
GROUP BY bill_no配合MAX(amount),虽然语法正确,但获取的是最大金额,而非最新金额——这两者在业务语义上完全不同。
归根结底,SQL开发中最容易踩的坑往往不是语法本身,而是对业务语言中“去重”二字的理解偏差。很多时候,业务方所说的“去重”已经隐含了“保留哪一条记录”的规则。然而,SQL标准中的DISTINCT和GROUP BY都未对保留哪条记录做出承诺。因此,在沟通需求时,务必追问一句:“需要保留的是哪一条?”
