今天咱聊聊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 編輯過