在 MySQL 的查询操作中,重复数据会直接影响统计结果的准确性,尤其当数据来源于多次导入、日志采集或多表关联时,同一业务实体可能以多条记录的形式存在。GROUP BY 作为分组查询的核心语法,天然具备去重能力,通过对一个或多个字段进行分组,能够让相同值的行归并为一组,每组只返回一条记录。通过合理的字段组合和聚合函数搭配,GROUP BY 可以实现不同维度的去重需求,同时还能输出组内统计信息。

GROUP BY 去重的核心原理
GROUP BY 的作用是将查询结果按照指定的一个或多个字段进行分组,相同字段值的行会被归为同一组,最终每组只会返回一条记录,这就实现了去重的效果。例如,如果只按照用户 ID 分组,那么相同的用户 ID 在结果集中只会出现一次,从而消除重复记录。分组字段的值完全相同,是 MySQL 判断两条记录是否属于同一组的唯一依据。
需要注意的是,GROUP BY 默认会保留每组的第一条记录,但这里所说的“第一条”并不是稳定可预测的业务顺序,而是依赖于存储引擎、索引扫描路径以及查询执行计划。若需要获取组内其他信息,比如订单数量、最大金额、最小创建时间等,必须搭配聚合函数使用,而不能直接选取组内非分组字段。否则 MySQL 在某些 SQL 模式下会直接报错,或者返回不确定的值。
此外,GROUP BY 默认会按照分组字段进行排序,这意味着查询结果通常会以分组字段的升序或默认字符集排序规则呈现。排序虽然让结果更易于阅读,但在大数据量环境下也可能带来额外的性能开销。理解这一机制,有助于后续在需要时采取关闭排序的优化手段。
单字段去重与基本语法
当只需要对单个字段进行去重时,直接在查询语句中添加 GROUP BY 字段即可,这种方式会保留每个分组的第一条数据。单字段分组是 GROUP BY 去重中最简单、最常见的用法,适合查询某个字段的全部不重复取值。
假设有一张用户订单表 user_order,表结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 订单ID |
| user_id | int | 用户ID |
| order_amount | decimal | 订单金额 |
| create_time | datetime | 创建时间 |
现在需要查询所有下过订单的用户 ID,并去除重复的用户 ID,对应的 SQL 语句如下:
-- 单字段GROUP BY去重,查询所有下过订单的用户ID SELECT user_id FROM user_order GROUP BY user_id;
这段语句按照 user_id 字段进行分组,结果集中每个用户 ID 只会保留一条记录。该写法只适合展示分组字段本身,如果还需要同时获取订单金额、创建时间等非分组字段,就必须使用聚合函数或进一步明确查询逻辑,否则查询结果可能不具备业务意义。
多字段组合去重
如果单字段去重无法满足业务要求,可以按照多个字段的组合进行分组去重。方法是在 GROUP BY 后面列出多个字段,字段之间使用逗号分隔。此时只有当所有指定字段的值都完全相同时,记录才会被判定为同一组,任意一个字段的值不同,都会被视为不同的组。
仍然以 user_order 表为例,现在需要查询每个用户每天的订单记录,去除同一用户在同一天的重复订单。由于 create_time 字段包含具体时间,直接按日期时间分组无法合并同一天的记录,因此需要结合 DATE() 函数将日期时间转换为日期,再与用户 ID 共同作为分组依据。
-- 多字段组合去重,查询每个用户每天的订单 SELECT user_id, DATE(create_time) AS order_date FROM user_order GROUP BY user_id, DATE(create_time);
上述 SQL 使用 DATE(create_time) 将日期时间值转换为日期,再与 user_id 共同分组。这样得到的结果中,一个用户在同一天只会出现一次,但不同日期或不同用户仍会分别保留,从而实现多维度组合去重。多字段分组在订单统计、日志分析以及用户行为分析等场景中非常常见。
搭配聚合函数获取组内信息
GROUP BY 去重后,每组只返回一条记录,但业务上常常还需要知道组内的其他信息,例如每组有多少条原始记录、最大金额是多少、最小金额是多少、总金额是多少等。此时需要搭配聚合函数使用。聚合函数会在分组完成后对每一组内部的数据进行计算,并把计算结果作为新的列返回。
常用的聚合函数包括 COUNT()、MAX()、MIN()、SUM()、AVG() 等。COUNT() 用于统计行数,MAX() 和 MIN() 分别获取最大值和最小值,SUM() 用于求和,AVG() 用于计算平均值。它们可以同时出现在 SELECT 列表中,也可以与分组字段共同构成查询结果。
下面示例查询每个用户的订单总数和最大订单金额,SQL 语句如下:
-- 搭配聚合函数获取组内统计信息
SELECT
user_id,
COUNT(id) AS order_count,
MAX(order_amount) AS max_amount
FROM user_order
GROUP BY user_id;
这段语句先按照 user_id 分组,然后通过 COUNT(id) 统计每个用户的订单数量,通过 MAX(order_amount) 获取每个用户的最大订单金额。GROUP BY 与聚合函数结合后,既能完成去重,也能输出组内汇总信息,这是单纯使用 DISTINCT 无法直接做到的。
GROUP BY 去重与 DISTINCT 去重的区别
很多开发者会混淆 GROUP BY 和 DISTINCT 的去重功能。两者在简单场景下看起来结果相似,但原理和适用场景存在明显差异。理解这些差异,有助于在实际查询中做出更合理的选择。
- DISTINCT 只能对查询的所有字段组合进行去重,无法搭配聚合函数做组内统计,而 GROUP BY 可以灵活搭配聚合函数获取组内信息。
- DISTINCT 的语法更简洁,适合简单的单字段或多字段组合去重场景;GROUP BY 更适合需要分组统计的场景,去重只是其功能之一。
- 在单字段去重时,两者的执行效率差异通常不大,优化器可能采用类似的执行计划;但在多字段复杂场景下,GROUP BY 的灵活性更高。
下面给出 DISTINCT 单字段去重的示例:
-- DISTINCT单字段去重 SELECT DISTINCT user_id FROM user_order;
该语句会返回所有不重复的用户 ID。与单字段 GROUP BY 相比,在没有聚合需求时,两者结果一致;但如果需要同时返回每个用户的订单数量、最大金额等统计信息,则必须使用 GROUP BY 而不是 DISTINCT。因此,DISTINCT 更偏向单纯的去重,GROUP BY 则兼有去重与分组计算的双重能力。
使用 GROUP BY 去重的注意事项
在使用 GROUP BY 进行去重时,需要注意以下规则,否则容易遇到查询报错或结果不符合预期的情况。
- GROUP BY 后面的字段必须出现在 SELECT 查询列表中,除非该字段被聚合函数包裹,否则 MySQL 在默认的严格模式下可能会报错。
- 如果需要筛选分组后的结果,不能使用 WHERE 子句,而要使用 HAVING 子句。WHERE 在分组前筛选数据,HAVING 在分组后筛选数据。
- GROUP BY 默认会对分组字段进行排序,若不需要排序可以添加
ORDER BY NULL来减少排序开销,提升查询效率。
示例:查询订单数大于 3 的用户 ID,使用 HAVING 进行分组后的筛选:
-- 使用HAVING筛选分组后的结果 SELECT user_id, COUNT(id) AS order_count FROM user_order GROUP BY user_id HAVING order_count > 3;
该语句先按 user_id 分组并计算每组订单数,然后通过 HAVING 子句保留订单数大于 3 的分组。如果这里的筛选条件写成 WHERE,MySQL 会因为聚合函数尚未计算而无法正确执行,因此必须区分 WHERE 和 HAVING 的作用阶段。
总体来说,GROUP BY 是 MySQL 中非常实用的去重与分组统计工具。对于简单的单字段去重,可以使用 GROUP BY 或 DISTINCT;对于需要同时返回组内数量、最大值、最小值、总和等统计信息的场景,GROUP BY 配合聚合函数是更合适的选择。编写 SQL 时,需要特别关注分组列、SELECT 列表以及 WHERE 与 HAVING 的搭配关系,这样才能在保证查询结果准确的同时,简化重复数据的处理逻辑,提升查询效率和可维护性。