一、为什么去重如此重要?
在深入技术细节之前,先明确去重的价值:
| 维度 | 影响 |
|---|---|
| 数据一致性 | 重复数据会导致统计结果失真,比如用户数虚高、订单金额重复计算 |
| 存储优化 | 冗余记录占用磁盘空间,拖慢查询速度 |
| 分析准确性 | BI报表、数据看板若基于重复数据,结论必然偏离事实 |
| 业务逻辑 | 唯一性约束被破坏,可能引发业务异常(如重复扣款) |
所以,去重不是"锦上添花",而是数据治理的基本功。
二、MySQL去重的六大核心方法
方法一:DISTINCT —— 最简洁的去重利器
DISTINCT是MySQL中最直观的去重关键字,直接作用于查询结果集,剔除所有重复行。
基础语法:
SELECT DISTINCT column_name FROM table_name;
多列组合去重:
SELECT DISTINCT user_name, email, address FROM t_user;
核心要点:
DISTINCT必须放在SELECT关键字之后、所有字段之前- 它作用于所有选中字段的组合,只有当所有字段值完全相同时才会被去重
- 适用于单列或少量字段的简单去重场景
性能表现: 在有索引的情况下,当结果集远小于表总量时,DISTINCT性能最优;但在大表无索引场景下,可能触发全表扫描,效率急剧下降。
方法二:GROUP BY —— 去重与聚合的双刃剑
GROUP BY通过分组实现去重,同时天然支持聚合函数,是分析型查询的首选。
基础语法:
SELECT user_name, email, address
FROM t_user
GROUP BY user_name, email, address;
配合聚合函数的进阶用法:
-- 查询每个部门的最高薪资
SELECT dept, MAX(salary) AS maximum
FROM staff
GROUP BY dept;
-- 查询雇员数超过2人的部门
SELECT dept, MAX(salary) AS maximum, MIN(salary) AS minimum
FROM staff
GROUP BY dept
HAVING COUNT(*) > 2;
GROUP BY vs DISTINCT 的关键区别:
| 对比项 | DISTINCT | GROUP BY |
|---|---|---|
| 语法复杂度 | 极简 | 中等 |
| 聚合能力 | 不支持 | 天然支持 |
| 返回非分组字段 | 有限制 | 需配合聚合函数 |
| 执行计划 | 某些场景与GROUP BY相同 | 某些场景与DISTINCT相同 |
性能结论: 有索引时,GROUP BY在结果集较大的场景下比DISTINCT性能更好;无索引时两者表现接近。
方法三:ROW_NUMBER() 窗口函数 —— 去重保留指定记录的终极方案
这是MySQL 8.0+版本引入的杀手级特性。当你需要"去重并保留最新/最早/最大/最小的那条记录"时,窗口函数是最佳选择。
核心Demo:保留每个用户的最新登录记录
WITH ranked AS (
SELECT
user_name,
email,
last_login,
ROW_NUMBER() OVER (PARTITION BY user_name ORDER BY last_login DESC) AS rn
FROM t_user
)
SELECT user_name, email, last_login
FROM ranked
WHERE rn = 1;
语法拆解:
PARTITION BY user_name:按用户名分组ORDER BY last_login DESC:组内按登录时间降序排列ROW_NUMBER():为每组内的行分配唯一序号WHERE rn = 1:只取每组的第一条(即最新记录)
低版本MySQL的替代方案(MySQL 5.7及以下):
SELECT user_name, email, address
FROM (
SELECT b.*,
@rownum := @rownum + 1,
IF(@pdept = b.user_name, @rank := @rank + 1, @rank := 1) AS rank,
@pdept := b.user_name
FROM (SELECT * FROM t_user ORDER BY user_name, last_login DESC) b,
(SELECT @rownum := 0, @pdept := NULL, @rank := 0) c
) t
WHERE rank = 1;
这种方式利用用户变量模拟ROW_NUMBER(),虽然能用,但可读性和可维护性远不如窗口函数。
方法四:临时表法 —— 大数据量去重的安全之选
当需要真正删除表中的重复数据(而非仅查询去重)时,临时表法是最稳妥的方案。
完整操作流程:
-- 步骤1:创建新表,插入去重数据
CREATE TABLE t_user_new LIKE t_user;
INSERT INTO t_user_new
SELECT * FROM t_user GROUP BY user_name, email;
-- 步骤2:验证数据
SELECT COUNT(*) FROM t_user_new;
-- 步骤3:替换原表
RENAME TABLE t_user TO t_user_backup, t_user_new TO t_user;
-- 步骤4:确认无误后删除备份
DROP TABLE t_user_backup;
优势: 不锁原表、不影响线上业务、可回滚,是生产环境大批量去重的首选方案。
方法五:DELETE + 子查询 —— 精准删除重复项
如果只想删除重复数据、保留一条(比如保留ID最小的那条):
DELETE FROM t_user
WHERE id NOT IN (
SELECT min_id FROM (
SELECT MIN(id) AS min_id
FROM t_user
GROUP BY user_name, email
) AS temp
);
注意: MySQL不允许在子查询中直接修改同一张表,所以必须再套一层SELECT,这是一个经典的"防坑"写法。
方法六:UNIQUE约束 —— 从源头杜绝重复
去重的最高境界不是"删",而是"防"。
ALTER TABLE t_user
ADD CONSTRAINT uk_user_email UNIQUE (user_name, email);
一旦添加唯一约束,任何插入重复数据的操作都会被MySQL拒绝并抛出错误。这是数据库层面的最后一道防线。
三、方法选型决策树
| 你的需求 | 推荐方法 | 理由 |
|---|---|---|
| 简单查询去重,不需要其他列 | DISTINCT |
语法最简,性能在小结果集时最优 |
| 需要聚合统计(count/max/sum等) | GROUP BY + HAVING |
天然支持聚合,功能最灵活 |
| 去重并保留最新/最大/最小的一条 | ROW_NUMBER() |
精确控制保留哪条,MySQL 8.0+首选 |
| 真正删除表中重复数据 | 临时表法 / DELETE+子查询 | 安全可控,支持回滚 |
| 防止未来产生重复 | UNIQUE约束 |
从根源解决问题 |
四、性能优化建议
- 建索引:对去重字段建立索引,能将全表扫描降为索引扫描,性能提升可达数倍
- 批量处理:大数据量去重时,分批次执行,避免长事务锁表
- 先查后删:执行任何删除操作前,务必先用
SELECT验证重复数据 - 定期维护:建议将去重纳入数据库定期维护计划,而非等问题爆发再处理
五、总结
MySQL的去重查询绝非"一招鲜吃遍天"。DISTINCT胜在简洁,GROUP BY赢在灵活,ROW_NUMBER()强在精准,临时表法贵在安全,UNIQUE约束赢在防患于未然。
作为开发工程师,真正的能力不在于记住所有语法,而在于面对具体场景时,能迅速判断哪种方法最合适、最安全、最高效。