按目标列分组后取每组首条记录最可靠,推荐用ROW_NUMBER()窗口函数配合PARTITION BY和ORDER BY实现;GROUP BY需配合聚合函数,仅SELECT DISTINCT无法实现单列去重。

用 GROUP BY 配合聚合函数取非重复列的对应值
直接对单列去重却想保留其他列数据,本质是「按目标列分组,再从每组中选一条记录」。SQL 不允许 SELECT * 配合 GROUP BY 除非所有非分组列都套上聚合函数——这不是偷懒,而是 SQL 标准强制要求的逻辑严谨性。
常见错误是写 SELECT DISTINCT col_a, col_b, col_c FROM t 想只让 col_a 去重,结果发现三列联合去重了,完全不是你要的效果。
- 如果其他列值在每组内其实一致(比如
col_a是用户 ID,col_b是用户名),用MIN(col_b)或MAX(col_b)安全且高效 - 如果其他列值不一致(比如同一
col_a对应多个不同时间戳),MIN()或MAX()就是在选“最早”或“最晚”的那条,得确认业务是否接受 - MySQL 5.7+ 默认开启
sql_mode=ONLY_FULL_GROUP_BY,不加聚合会直接报错:Expression #2 of SELECT list is not in GROUP BY clause
用窗口函数 ROW_NUMBER() 选每组第一条(推荐)
更可控的方式是用 ROW_NUMBER() 给每组内的行编号,再筛出序号为 1 的记录。它不依赖其他列是否重复,也不强制聚合,语义清晰。
示例:按 email 去重,保留首次注册的完整记录
SELECT id, email, name, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at) AS rn
FROM users
) t
WHERE rn = 1;
-
PARTITION BY email表示按email分组 -
ORDER BY created_at决定哪条算“第一条”,可换成id、updated_at等,按需调整 - PostgreSQL、SQL Server、Oracle、MySQL 8.0+、SQLite 3.25+ 都支持;旧版 MySQL 只能靠变量模拟,容易出错
用关联子查询或 EXISTS 实现“取最新一条”
某些场景下不想用窗口函数(比如兼容老版本),可用相关子查询:对每一行,检查是否存在另一行满足“同 key 但时间更新”,不存在就留下它。
示例:保留每个 product_id 下 updated_at 最大的那条
SELECT t1.*
FROM products t1
WHERE NOT EXISTS (
SELECT 1
FROM products t2
WHERE t2.product_id = t1.product_id
AND t2.updated_at > t1.updated_at
);
- 性能敏感时注意:没索引的话,
t2.product_id和t2.updated_at必须有联合索引,否则全表扫描爆炸 - 如果存在多条
updated_at相同的记录,这个写法会全部保留——和ROW_NUMBER()行为不同,得看业务是否允许 - 比
GROUP BY + MAX()更灵活,但写法略长,可读性稍低
别踩 DELETE 时的“只删重复行”陷阱
如果目标是“原表去重”,而不是查出来,很多人会想 DELETE 配 GROUP BY,但绝大多数数据库不支持这种写法,MySQL 会报错:You can't specify target table 't' for update in FROM clause。
- 安全做法是先用
ROW_NUMBER()找出要删的id,再DELETE FROM t WHERE id IN (...) - 或者用临时表:把去重后数据
INSERT INTO tmp SELECT ...,再TRUNCATE t后倒回去 - 线上表务必先备份,
DELETE没WHERE条件的事故太多了
真正麻烦的从来不是语法怎么写,而是你得先确认:所谓“保留其他列”,到底是保留任意一条、最新一条、最早一条,还是某列值最大的那条——业务含义模糊时,代码越“巧妙”,越容易埋雷。










