distinct去不掉“看起来重复”的数据,是因为它按整行所有列值完全一致判断重复,而非按业务感知的局部字段;如name相同但id或created_at不同,仍视为不同行。

为什么 DISTINCT 有时去不掉“看起来重复”的数据
因为 DISTINCT 判断重复是基于**整行所有列的值完全一致**,不是按某几列“肉眼觉得重复”。比如两行数据在 name 和 email 上相同,但 created_at 差1秒,或 id 不同,DISTINCT 就会保留这两行。
常见错误现象:SELECT DISTINCT name FROM users 返回了多个同名用户——这不是 bug,是设计行为。只要其他列(哪怕没选出来)存在差异,数据库仍视其为不同行,只是投影后你“看不见”那些差异列。
- 若想按
name去重,必须只查name,且确保没有隐藏的隐式差异(如尾部空格、大小写、不可见字符) - MySQL 默认不区分大小写,PostgreSQL 区分;用
TRIM()和LOWER()预处理可规避部分问题 -
DISTINCT在执行计划中通常触发排序或哈希去重,大数据量时性能明显下降
DISTINCT 和 GROUP BY 的关键区别在哪
DISTINCT 是结果集去重操作,不支持聚合;GROUP BY 是分组逻辑,天然支持 COUNT()、MAX() 等聚合函数。两者语义不同,不能简单互换。
典型误用:SELECT DISTINCT name, COUNT(*) FROM users —— 语法直接报错,因为 COUNT(*) 需要 GROUP BY name。
- 要统计每个
name出现次数:用SELECT name, COUNT(*) FROM users GROUP BY name - 只要去重后的
name列表:用SELECT DISTINCT name FROM users即可,更轻量 - 某些场景下
GROUP BY可替代DISTINCT(如SELECT name FROM users GROUP BY name),但语义模糊,不推荐用于单纯去重
多字段 DISTINCT 的陷阱:NULL 怎么算重复
SQL 标准规定:两个 NULL 值在 DISTINCT 中被视为相等。但实际行为受数据库影响——例如 PostgreSQL 严格遵循标准,而旧版 MySQL 在某些模式下可能表现异常。
常见问题:含 NULL 的字段参与 DISTINCT,导致结果比预期少(多个 NULL 被合并成一个)。
- 若业务要求把
NULL当作不同值处理,需提前转换:如COALESCE(status, 'UNKNOWN') - 多字段组合时,
(a=1, b=NULL)和(a=1, b=NULL)算重复;但(a=1, b=NULL)和(a=1, b='')永远不重复 - 避免在
DISTINCT中混用NULL和空字符串,它们是不同值
替代 DISTINCT 的更可控方案:ROW_NUMBER() + CTE
当需要“按某列去重,同时保留该组中最新/最旧的一条完整记录”时,DISTINCT 完全无能为力。这时必须用窗口函数。
例如:每个 email 只取 id 最大的那条用户记录。
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id DESC) AS rn FROM users ) SELECT id, name, email FROM ranked WHERE rn = 1;
-
ROW_NUMBER()确保每组内顺序唯一,不会因排序字段重复导致随机取值 - 相比
GROUP BY + MAX(id)再 JOIN,CTE 方案更直观、易维护 - 注意:SQLite 3.25+、PostgreSQL 8.4+、MySQL 8.0+ 支持窗口函数;旧版 MySQL 必须用变量模拟,稳定性差
真正麻烦的从来不是“怎么写 DISTINCT”,而是“你以为重复,其实数据库认为不重复”——查之前先 SELECT * FROM table LIMIT 5 看原始数据,比硬调 DISTINCT 有效得多。











