SQL DISTINCT 与去重技巧解析

2次阅读

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

SQL DISTINCT 与去重技巧解析

SELECT DISTINCT 是怎么去重的?

DISTINCT 不是“挑出某列不重复的值”,而是对 整个 SELECT 行的所有字段组合 做唯一性判断。比如 SELECT DISTINCT a, b FROM t,只有当 ab 同时相等时,才算重复行。

  • NULL 被视为相同值:两行都是 (1, NULL),会被去成一行
  • 它不保证返回顺序:即使原表按 id 递增,DISTINCT 结果可能乱序,除非显式加 ORDER BY
  • 不能只对部分列生效:想“按 user_id 去重,但保留最新一条的 created_at”,DISTINCT 办不到——它没排序控制能力
  • 底层常触发排序或哈希去重,大数据量时容易内存溢出或变慢

GROUP BY 替代 DISTINCT 的真实用途

语法上 SELECT col FROM t GROUP BY colSELECT 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):先分组,再组内排序编号
  • 示例:保留每个 departmentid 最小的员工记录:
    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 + 聚合。兼容性和可维护性,往往比写得短更重要。

text=ZqhQzanResources