今天咱聊聊SQL里那些去重的招儿。平时查数据总遇到重复值,统计结果跑偏、存了一堆没用的,头疼得很。我整理了6个常用的去重关键字和操作,结合实际场景给你掰扯明白,保准你看完就知道该用哪个!
一、DISTINCT:单表去重的"快刀"
这玩意儿最简单,直接把重复的行干掉。但记住一点:它是看所有选中列的组合,不是单看某一列。
举个例子,员工表employees
里有这些数据:
id name department
1 Alice HR
2 Bob Engineering
3 Alice HR -- 这行和1行name+department重复
4 Charlie Marketing
用DISTINCT
查姓名:
SELECT DISTINCT name FROM employees;
结果就只剩Alice、Bob、Charlie
,重复的Alice
被合并了。
要是查name+department
:
SELECT DISTINCT name, department FROM employees;
因为第3行和第1行的name+department
完全一样,所以只留一行。
优点:简单直接,一行代码搞定单表去重。
坑点:会隐式排序,数据量大了可能变慢;而且作用于所有选中的列,想只去重某一列但保留其他列?不行!
二、GROUP BY:带统计功能的"去重+计算器"
GROUP BY
比DISTINCT
灵活,不光能去重,还能顺带做统计(比如计数、求和)。
还是用上面的员工表,想统计每个部门有多少人:
SELECT department, COUNT(*) AS 人数
FROM employees
GROUP BY department;
结果会是:
HR 2
Engineering 2
Marketing 1
这其实就是按department
分组,每组只留一条(去重),再算每组的数量。
和DISTINCT的区别:
DISTINCT
只去重,不统计;GROUP BY
能分组+计算,适合需要分析数据的场景。- 大数据量时
GROUP BY
性能可能更好(尤其加了索引的话),因为它能按分组字段有序处理。
三、UNION vs UNION ALL:多表合并时的"过滤器"
这俩是用来合并多个查询结果的,比如查两个部门的员工,再合并到一起。
先建两个表:
-- 人力资源部
hr_dept: id=1(Alice), id=2(David)
-- 工程部
eng_dept: id=2(David), id=3(Bob), id=1(Alice)
用UNION
合并:
SELECT name FROM hr_dept
UNION
SELECT name FROM eng_dept;
结果是Alice、David、Bob
——重复的Alice
和David
被自动去重了。
换成UNION ALL
:
SELECT name FROM hr_dept
UNION ALL
SELECT name FROM eng_dept;
结果会是Alice、David、David、Bob、Alice
——原样保留所有重复项,不做去重。
用法口诀:
- 确定没重复,或者不需要去重?用
UNION ALL
,速度快(少了去重步骤)。 - 怕有重复,必须去重?用
UNION
,但性能会差一点(要排序去重)。
四、EXCEPT / MINUS:找"差异"的利器
这俩是求差集:返回"第一个表有,第二个表没有"的记录,而且自动去重。
比如有两个表:
all_products(所有产品): id=1(Laptop), 2(Phone), 3(Tablet)
sold_products(已售产品): id=1(Laptop), 3(Tablet)
想查"没卖出去的产品",用EXCEPT
(SQL Server/PostgreSQL):
SELECT * FROM all_products
EXCEPT
SELECT * FROM sold_products;
结果就一个id=2(Phone)
。
注意:Oracle用MINUS
替代EXCEPT
;MySQL不支持这俩,得用LEFT JOIN
模拟:
-- MySQL替代方案
SELECT ap.*
FROM all_products ap
LEFT JOIN sold_products sp ON ap.id = sp.id
WHERE sp.id IS NULL; -- 只留没匹配上的
五、INTERSECT:找"交集"的工具
返回两个表都有的记录,自动去重。比如查"所有会员"和"活跃会员"的重叠部分:
all_members(所有会员): id=1(Tom), 2(Jerry), 3(Spike)
active_members(活跃会员): id=1(Tom), 3(Spike), 4(Tyke)
用INTERSECT
(Oracle/SQL Server支持):
SELECT * FROM all_members
INTERSECT
SELECT * FROM active_members;
结果是id=1(Tom)、3(Spike)
——这俩在两个表都出现了。
MySQL同样不支持,用INNER JOIN
模拟:
SELECT am.*
FROM all_members am
INNER JOIN active_members ac ON am.id = ac.id;
六、窗口函数:复杂去重的"手术刀"
上面的方法对付简单场景够了,但遇到"保留每组最新一条"这种需求,就得用ROW_NUMBER()
这类窗口函数了。
比如订单表orders
,想按用户分组,只留每个用户最新的订单:
id user_id order_time
1 100 2023-01-01
2 100 2023-01-05 -- 这个用户的最新订单
3 200 2023-01-03
用ROW_NUMBER()
给每组排序,再取第一条:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn
FROM orders
) t
WHERE rn = 1; -- 只留每组排序第一的(最新的)
结果会留下id=2
(user_id=100的最新订单)和id=3
(user_id=200的唯一订单)。
不同数据库的"脾气"得注意
有些方法不是所有数据库都支持,比如:
- MySQL没有
EXCEPT
和INTERSECT
,得用JOIN
模拟; ROW_NUMBER()
在MySQL 8.0+才支持,老版本不行。
场景速查表:啥时候用啥招?
| | |
---|
| | SELECT DISTINCT dept FROM emp |
| | SELECT dept, COUNT(*) FROM emp GROUP BY dept |
| | SELECT name FROM A UNION SELECT name FROM B |
| | |
| | SELECT * FROM A EXCEPT SELECT * FROM B |
| | SELECT * FROM A INTERSECT SELECT * FROM B |
| | PARTITION BY user_id ORDER BY time DESC |
最后说句大实话
去重不是越复杂越好,得看数据量和需求:
- 多表合并:优先
UNION ALL
(快),需要去重再用UNION
; - 复杂逻辑(如保留最新记录):窗口函数
ROW_NUMBER()
是王道。
记住这些,下次遇到重复数据,你就知道该拔刀还是用手术刀了!
该文章在 2025/7/31 9:21:50 编辑过