select distinct 对整行字段组合去重,null视为相同值,不保证顺序;group by 可结合聚合函数精准取值;row_number() 窗口函数最可靠,支持按组排序留指定记录。

SELECT DISTINCT 是怎么去重的?
DISTINCT 不是“挑出某列不重复的值”,而是对 整个 SELECT 行的所有字段组合 做唯一性判断。比如 SELECT DISTINCT a, b FROM t,只有当 a 和 b 同时相等时,才算重复行。
- NULL 被视为相同值:两行都是
(1, NULL),会被去成一行 - 它不保证返回顺序:即使原表按
id递增,DISTINCT结果可能乱序,除非显式加ORDER BY - 不能只对部分列生效:想“按
user_id去重,但保留最新一条的created_at”,DISTINCT办不到——它没排序控制能力 - 底层常触发排序或哈希去重,大数据量时容易内存溢出或变慢
GROUP BY 替代 DISTINCT 的真实用途
语法上 SELECT col FROM t GROUP BY col 和 SELECT DISTINCT col FROM t 效果一样,但 GROUP BY 才是“能干活”的那个。
- 必须把所有非聚合字段都写进
GROUP BY,否则多数数据库(如 PostgreSQL、SQL Server)直接报错;MySQL 在宽松模式下可能放行,但返回哪条记录不确定 - 真正价值在于结合聚合函数:比如
SELECT user_id, MAX(created_at) FROM events GROUP BY user_id,既能去重又能取最新时间 - 性能通常比
DISTINCT更可控——尤其当你已有user_id索引时,GROUP BY可走索引扫描,而DISTINCT可能被迫建临时哈希表
ROW_NUMBER() 实现精准去重(保留最新/最小 ID)
当你需要“每组只留一条,且明确指定留哪条”,就得用窗口函数。这是目前最可靠、语义最清晰的方案。
- 核心是
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col):先分组,再组内排序编号 - 示例:保留每个
department中id最小的员工记录:WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY department ORDER BY id) AS rn FROM employees ) SELECT * FROM ranked WHERE rn = 1;
- 注意
ORDER BY方向:要最新记录就用ORDER BY created_at DESC;要最早就用ASC - 不是所有环境都支持:SQLite 3.25+、MySQL 8.0+、PostgreSQL、Oracle、SQL Server 都行;老版本 MySQL 或某些 OLAP 引擎(如 Presto 0.215 之前)需换
ROW_NUMBER()为RANK()或子查询模拟
别在 COUNT(DISTINCT ...) 上踩兼容性坑
COUNT(DISTINCT col) 看似简单,但实际部署时最容易翻车。
- MySQL、PostgreSQL、SQL Server 支持;但旧版 Hive(near "DISTINCT": syntax error
- 替代写法统一可用:
SELECT COUNT(*) FROM (SELECT DISTINCT col FROM t) AS _ - 多列去重计数(如 UV 统计)更危险:
COUNT(DISTINCT user_id, platform)在 MySQL 8.0+ 和 PostgreSQL 9.5+ 支持,但 Hive 只认单列,强行写会解析失败 - 大数据量时,
COUNT(DISTINCT)内部仍要哈希去重,内存峰值可能飙升——线上任务突然 OOM,十有八九是它
实际用的时候,别默认选 DISTINCT;先问自己一句:我到底是要“纯粹筛掉重复行”,还是要“每组挑一条”?前者勉强可用,后者必须上 ROW_NUMBER() 或 GROUP BY + 聚合。兼容性和可维护性,往往比写得短更重要。










