DISTINCT按整行去重,字段值任意不同即视为不重复;需按业务规则保留某条记录时应使用ROW_NUMBER()窗口函数配合PARTITION BY和ORDER BY。

用 DISTINCT 去重时,为什么结果还是有“重复”?
因为 DISTINCT 是对整行去重,不是按某个字段。只要任意一列值不同,两行就被视为不重复。
常见错误现象:SELECT DISTINCT name, email, created_at FROM users 返回了多个同名同邮箱的记录——因为 created_at 时间戳不同。
- 如果只想按
name和email去重,必须先决定保留哪一条(比如最新/最早),DISTINCT本身不支持这个逻辑 -
DISTINCT无法跳过 NULL 值特殊处理:两个NULL在多数数据库中被视为“相等”,但有些场景下(如 GROUP BY 后聚合)行为不一致 - 性能影响:全字段去重会加大排序/哈希开销,尤其在宽表或大结果集上比只选关键字段慢不少
ROW_NUMBER() 窗口函数去重的实际写法
真正可控的去重靠 ROW_NUMBER() 配合子查询或 CTE,核心是“分组 + 排序 + 取第 1 条”。
使用场景:需要按业务规则保留某条记录,比如每个用户只留最新登录记录、每个订单只留首条发货明细。
示例(PostgreSQL / SQL Server / MySQL 8.0+):
SELECT id, user_id, order_time, status
FROM (
SELECT id, user_id, order_time, status,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn
FROM orders
) t
WHERE rn = 1;
-
PARTITION BY user_id定义“去重维度”,相当于“每组内独立编号” -
ORDER BY order_time DESC决定谁排第一;升序则取最早,降序取最新 - 注意:MySQL 5.7 或更早不支持窗口函数,强行用会报错
ERROR 1064 - 如果分区键含 NULL,不同数据库处理方式不同(如 PostgreSQL 把 NULL 视为同一组,Oracle 默认不合并)
DISTINCT 和 ROW_NUMBER() 的选择依据
别凭感觉选,看数据语义和控制粒度。
- 纯字段组合唯一性校验、导出报表初筛、临时查重 —— 用
DISTINCT,简单直接 - 要保留完整行且必须指定优先级(如“最新”“最高金额”“非空邮箱优先”)—— 必须用
ROW_NUMBER()或类似窗口函数 - 如果只是想删库里的重复数据,
ROW_NUMBER()+DELETE是主流做法,但注意某些数据库(如 MySQL)不允许在子查询中直接 DELETE 同一张表,得套一层 CTE 或临时表 - 兼容性提醒:SQLite 直到 3.25.0 才支持窗口函数,旧版本只能用关联子查询模拟,性能差很多
容易被忽略的 NULL 处理和索引影响
去重逻辑里,NULL 不是“空值”,它是未知值,在比较和分组中行为特殊。
-
DISTINCT中,(1, NULL)和(1, NULL)通常算重复;但(1, NULL)和(1, 'a')肯定不重复 -
ROW_NUMBER() OVER (PARTITION BY col):若col是 NULL,大多数数据库会把所有 NULL 值归为同一组(PostgreSQL / SQL Server 是这样;Oracle 需显式写col IS NULL才能捕获) - 性能陷阱:没给
PARTITION BY字段建索引,ROW_NUMBER()会强制全表扫描+排序,百万级表可能秒变几十秒 - 如果业务允许,提前用
WHERE col IS NOT NULL过滤,能显著减少窗口计算量
DISTINCT 简单但死板,ROW_NUMBER() 灵活但得写清楚排序逻辑,而 NULL 和索引这两块,十次有八次会漏掉。










