SQL 中 DISTINCT 和 GROUP BY 有什么区别?
简化版
DISTINCT 用来对查询结果去重,GROUP BY 用来按分组键聚合;只想去重优先看 DISTINCT,需要 COUNT/SUM/MAX 这类汇总时使用 GROUP BY。
详细版
DISTINCT 作用在 SELECT 结果列上,它会把结果集中完全相同的行合并成一行。比如 SELECT DISTINCT city FROM users 表示拿到不重复的城市。
GROUP BY 会把表中的行按一个或多个字段分成若干组,然后每组输出一行,通常配合聚合函数使用。比如 SELECT city, COUNT(*) FROM users GROUP BY city 表示统计每个城市有多少用户。
二者在某些简单场景中结果相同,例如 SELECT DISTINCT city FROM users 和 SELECT city FROM users GROUP BY city 都能拿到城市列表。但语义不同:前者表达“结果去重”,后者表达“按城市分组”。面试中应优先强调语义,再补充执行计划可能相近,具体优化取决于数据库实现、索引和数据分布。
完整版教学
一、先把两个语义拆开
DISTINCT 的核心是“结果集去重”。数据库先根据查询条件得到候选行,再看 SELECT 输出的列组合是否重复,重复的组合只保留一份。
GROUP BY 的核心是“分桶聚合”。它先把行按分组键放入不同组,再对每个组计算聚合函数,最后每个组返回一行。
用同一张表看差异更直观:
-- users
-- id | city | age
-- 1 | 北京 | 20
-- 2 | 北京 | 21
-- 3 | 上海 | 20
SELECT DISTINCT city FROM users;
SELECT city, COUNT(*) AS user_count
FROM users
GROUP BY city;
第一个查询只关心城市是否重复,第二个查询关心每个城市背后有多少行。
记忆钩子:
DISTINCT看“输出行是否重复”,GROUP BY看“原始行应该分到哪个组”。
二、为什么有时结果看起来一样
当查询只输出分组键本身,并且没有聚合函数时,GROUP BY 的输出天然也是每个分组一行。
例如下面两条 SQL 在结果上可能一致:
SELECT DISTINCT city FROM users;
SELECT city
FROM users
GROUP BY city;
如果原表有 10000 行,城市只有 20 个,两个查询都可能返回 20 行。但这不代表它们完全等价,尤其当你再增加列、聚合函数、排序、过滤时,语义差异会放大。
| 写法 | 主要目的 | 常见搭配 | 面试表达 |
|---|---|---|---|
DISTINCT | 去掉重复结果 | 普通查询列 | 输出唯一值 |
GROUP BY | 分组后计算 | COUNT/SUM/MAX/MIN/AVG | 每组一行 |
GROUP BY 无聚合 | 可实现去重 | 分组键 | 可读性通常弱于 DISTINCT |
三、多列去重时要看“列组合”
DISTINCT 不是只对第一列去重,而是对整个输出列组合去重。
SELECT DISTINCT city, age
FROM users;
如果数据是:
北京,20
北京,21
北京,20
上海,20
结果会保留 3 行:北京,20、北京,21、上海,20。这就是很多人误以为“按 city 去重”却多出行数的原因。
如果你真正想要“每个城市只保留一行,并且带上某个用户年龄”,就不能随便 DISTINCT city, age,而要定义保留规则,例如最小年龄、最大年龄或最新一条记录。
SELECT city, MIN(age) AS min_age
FROM users
GROUP BY city;
四、GROUP BY 的选择列规则更严格
标准 SQL 中,SELECT 里出现的非聚合列通常必须在 GROUP BY 中出现,否则数据库不知道每组多行里该取哪一个值。
-- 推荐:每个非聚合列都能解释来源
SELECT city, COUNT(*) AS user_count
FROM users
GROUP BY city;
-- 风险:name 不是分组键,也不是聚合结果
SELECT city, name, COUNT(*)
FROM users
GROUP BY city;
第二条 SQL 的问题是:北京这一组可能有 2 个用户,name 应该取谁?有些数据库会直接报错,有些数据库在特定模式下允许但结果不可靠。
执行逻辑可以这样理解:
原始行 -> 按 city 分组 -> 每组计算 COUNT(*) -> 输出 city + count
这个流程里没有一步能自然产生“任意 name 但仍然正确”的含义。
五、性能不能只靠关键字判断
面试中常见追问是:DISTINCT 和 GROUP BY 谁更快?更好的回答是:它们可能走相似的去重/分组算法,性能取决于索引、排序、哈希聚合、数据量和输出列宽。
数据库实现上常见两类路径:
排序去重:读取 -> 按 key 排序 -> 相邻重复合并
哈希去重:读取 -> key 放入 hash 表 -> 已存在则跳过
假设 100 万行、城市 20 个,如果有 (city) 索引,数据库可能沿索引顺序快速得到唯一城市。如果输出列很多且没有合适索引,DISTINCT 需要比较更宽的列组合,成本就会上升。
因此写 SQL 时先保证语义准确,再用 EXPLAIN 看执行计划,而不是机械背“某个关键字一定更快”。
六、真实业务里怎么选
只拿唯一值列表时,优先用 DISTINCT,因为它把意图表达得最清楚。
SELECT DISTINCT department_id
FROM employees
WHERE status = 'active';
需要每组指标时,用 GROUP BY:
SELECT department_id, COUNT(*) AS active_count
FROM employees
WHERE status = 'active'
GROUP BY department_id;
需要“每组取一条完整记录”时,不要用 DISTINCT 碰运气,应该用窗口函数或明确聚合规则。
SELECT *
FROM (
SELECT e.*,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY hired_at DESC, id DESC
) AS rn
FROM employees e
) t
WHERE rn = 1;
七、常见误区与追问
- 误区:
DISTINCT city, age表示只按city去重。 它按输出列组合去重,age不同就会保留多行。 - 误区:
GROUP BY一定比DISTINCT快。 两者可能使用类似算法,必须结合索引和执行计划判断。 - 误区:没有聚合函数也应该用
GROUP BY去重。 可以做到,但表达“去重”时DISTINCT通常更直观。 - 误区:分组后随便选择非分组列也没问题。 多行合成一组后,非分组列必须有明确来源,否则结果不稳定或直接报错。
- 追问:如何每组取最新一条? 常用
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...),然后取rn = 1。 - 追问:多列唯一值列表怎么写? 用
SELECT DISTINCT col1, col2 ...,它返回唯一的列组合。
八、加强记忆
记住两条线:DISTINCT 是站在“最终输出行”的角度去重,GROUP BY 是站在“原始数据分组”的角度汇总。看到“唯一列表”想到 DISTINCT,看到“每类多少、每类最大、每类平均”想到 GROUP BY,看到“每组保留一条完整记录”再进一步想到窗口函数。