一、一个被问了无数次的问题
在 SQL 编写中,去重是最基础也最高频的操作之一。当你需要从一张表中提取不重复的字段值时,脑海里往往同时浮现两个关键字:DISTINCT 和 GROUP BY。
它们看起来殊途同归,都能把重复的行过滤掉,只留下唯一值。但在性能层面,两者的表现可能天差地别。很多开发者凭经验选一个,觉得"反正结果一样"。直到某天数据量暴涨,查询从毫秒级退化到秒级,才意识到当初那个随意的选择,埋下了一个性能隐患。
这篇文章,我们从执行计划、内部机制、多场景实测三个维度,把 DISTINCT 和 GROUP BY 在去重场景下的差异彻底拆开,给你一份可直接用于生产环境的选型指南。
二、先搞清底层:它们真的一样吗
从 SQL 语法的语义上看,DISTINCT 和 GROUP BY 在单纯去重时确实等价。但数据库引擎对它们的处理路径,存在根本性的差异。
DISTINCT 的执行逻辑
DISTINCT 的核心动作是"去重"。数据库引擎在扫描数据时,会维护一个哈希表(或排序结构),每读到一行就检查该行的去重字段是否已经存在。如果不存在,就保留;如果已存在,就丢弃。整个过程只关心"这个值出现过没有",不关心出现了几次,也不做任何聚合运算。
在执行计划中,DISTINCT 通常对应一个"去重"节点,有时也会被优化为"排序去重"或"哈希去重",取决于数据量和内存情况。
GROUP BY 的执行逻辑
GROUP BY 的核心动作是"分组"。它把相同值的行归到一组,然后对每一组执行聚合操作。即便你不写任何聚合函数,数据库引擎也会为每一组保留一条记录。这意味着 GROUP BY 在内部多做了一步:先分组,再从每组中选出一条代表。
在执行计划中,GROUP BY 通常对应一个"分组聚合"节点,涉及排序或哈希分组,以及分组后的数据提取。
关键差异就在这里:DISTINCT 只需要判断"是否出现过",而 GROUP BY 需要完成"分组 + 提取代表行"两步。多出来的这一步,在小数据量下几乎无感,但在大数据量下,可能成为性能分水岭。
三、实测设计:控制变量,逐项对比
为了得到客观结论,我们设计了三组典型场景,分别在不同数据规模下测试 DISTINCT 和 GROUP BY 的执行耗时。测试环境为单表,字段类型为整型和字符串,数据量分别设置为十万级、百万级、千万级。
场景一:单字段去重,无索引
这是最常见的情况:从一张大表中提取某个字段的唯一值。数据完全无序,没有任何索引辅助。
结果显示:在十万级数据时,两者耗时几乎一致,差距在百分之五以内。但到了百万级,DISTINCT 开始出现优势,平均快百分之八到十二。到了千万级,差距拉大到百分之十五到二十。DISTINCT 始终略胜一筹。
原因很直接:DISTINCT 的哈希去重在遇到重复值时可以立即丢弃,而 GROUP BY 即便不做聚合,也要先完成分组结构的构建,多了一层开销。
场景二:单字段去重,有索引
当去重字段上存在索引时,情况发生了变化。数据库可以直接沿索引树扫描,天然有序,去重变得极其高效。
在这个场景下,DISTINCT 和 GROUP BY 的耗时都大幅下降,但 GROUP BY 的降幅更大。原因在于:GROUP BY 可以利用索引的有序性直接完成分组,而 DISTINCT 虽然也能走索引,但其内部的去重判断逻辑在有序数据下并没有比 GROUP BY 省去多少步骤。两者差距缩小到百分之三以内,基本可以视为持平。
场景三:多字段组合去重
当去重条件涉及两个或以上字段时,比如按"城市 + 类别"组合去重,情况又有不同。
DISTINCT 需要对组合字段构建复合哈希键,内存占用上升。GROUP BY 同样需要复合分组,但其分组逻辑在多字段场景下与 DISTINCT 的差异进一步缩小。实测中,两者耗时几乎完全一致,差距不超过百分之二。
这说明:当去重维度增加时,DISTINCT 的轻量优势被稀释,两者趋于等价。
四、执行计划里的秘密:看懂这几个关键词
如果你不想跑实测,也可以通过执行计划快速判断该用哪个。关注以下几个关键词:
DISTINCT 对应的计划节点
通常出现"Unique"或"Distinct"字样。如果走哈希去重,会看到"Hash Aggregate";如果走排序去重,会看到"Sort + Unique"。哈希去重在数据量适中时效率更高,排序去重在数据已有序或内存不足时更稳定。
GROUP BY 对应的计划节点
通常出现"Group By"或"Aggregate"字样。如果不带聚合函数,执行计划中仍会有分组操作,但不会出现"Sum""Count"等聚合计算。如果你看到执行计划里有明确的聚合函数计算,说明引擎在做多余的工作——这时候换成 DISTINCT 会更优。
一个关键判断点:如果执行计划中 GROUP BY 的节点后跟着聚合计算(哪怕是计数),而你实际上不需要这个计数,那就果断换成 DISTINCT。这一步看似微小,在大表上可能节省可观的时间。
五、选型指南:什么时候用哪个
根据上述分析和实测结论,我们可以总结出一条清晰的选型路径:
优先选 DISTINCT 的场景:
- 单纯去重,不需要任何聚合信息
- 单字段或少量字段去重
- 数据量较大,且无合适索引
- 对执行效率有较高要求
DISTINCT 语义清晰,执行路径更短,在大多数去重场景下是更优选择。它告诉数据库"我只要唯一值,别的不要",引擎会按最简洁的路径执行。
可以选 GROUP BY 的场景:
- 去重的同时需要聚合信息,比如去重后统计每组的数量
- 多字段组合去重,且后续需要对每组做进一步处理
- 去重字段有索引,且你习惯用 GROUP BY 的写法
- 需要配合 HAVING 子句做分组后过滤
注意:如果你用 GROUP BY 仅仅是为了去重,而不需要任何聚合,这在语义上是一种"滥用"。虽然数据库能正确执行,但从代码可读性和执行效率两个角度,DISTINCT 都是更好的选择。
一个容易踩的坑:
有些开发者会写出"SELECT 字段, COUNT() FROM 表 GROUP BY 字段"来实现去重加计数。如果你只需要去重,不需要计数,就不要写 COUNT()。这个多余的聚合操作会让执行计划多出一个计算步骤,在大数据量下影响明显。
六、进阶话题:去重之外的考量
除了性能,还有几个维度值得关注。
可读性与维护性
DISTINCT 的语义一目了然:去重。GROUP BY 的语义是分组,用它来去重属于"曲线救国"。当你的同事接手这段代码时,看到 DISTINCT 会立刻明白意图;看到 GROUP BY 则需要多想一步:"他是要分组还是要去重?"在团队协作中,语义明确的写法永远优于语义模糊的写法。
与窗口函数的配合
在复杂查询中,DISTINCT 和 GROUP BY 与窗口函数的交互方式不同。如果你的去重逻辑需要结合行号、排名等窗口计算,通常需要先用子查询或 CTE 做去重,再套窗口函数。这种情况下,用哪个关键字去重对最终性能影响不大,关键在于子查询的写法是否合理。
数据库引擎的优化差异
不同数据库引擎对 DISTINCT 和 GROUP BY 的优化策略不同。有些引擎会把 GROUP BY(不带聚合)自动重写为 DISTINCT,有些则不会。这意味着在某些数据库中,你写 GROUP BY 去重和写 DISTINCT 去重,最终执行计划可能完全一样。但你不能依赖这种隐式优化,因为换一个数据库,行为可能就变了。显式写出你的意图,才是可迁移、可预期的做法。