多列去重与每组取一行
DISTINCT 作用于它后面的整个列清单,去重的是列的组合:只要任何一列不同,两行就被当成不同数据。日常需求里更常见的是另一种形态——每个分组只保留一行,比如每个城市只取一条代表记录。这件事必须附带一个选择规则,否则结果不可预期。PostgreSQL 为此提供了专门语法 DISTINCT ON,MySQL 没有对应写法,只能用窗口函数给每行打行号再筛出行号等于 1。两种写法结果可以完全一致,差别在可读性、执行方式与迁移成本。
基本信息
| 适用平台 | MySQL 8.0 / PostgreSQL 12 及以上 |
|---|---|
| 多列去重 | 列清单写在 DISTINCT 之后,按组合去重,任一列不同即为不同行 |
| 每组取一行 | PostgreSQL 用 DISTINCT ON 配合排序,决定保留哪一行 |
| MySQL 等价改写 | 用窗口函数按分组打行号,外层筛行号等于 1 |
| 组合个数统计 | 统一写成子查询:先取得去重结果,外层再计数,两库都能跑 |
典型使用场景
- 导出唯一的「城市加状态」组合清单,作为筛选下拉框的选项
- 每个城市保留一条代表记录:注册最早、金额最大或时间最新的一条
- 组合个数统计:关心有多少种不同组合,而不是表里有多少行
- 列表页按业务键去重后再分页,避免同一对象在相邻页重复出现
- 从 PostgreSQL 迁移到 MySQL 时把 DISTINCT ON 批量改写成窗口函数
- 核对上游重复数据:先看去重前后行数差多少,再决定是加唯一约束还是清洗数据
三种写法
三种写法
-- 一、多列组合去重:任一列不同即为不同行
SELECT DISTINCT city, status FROM users;
-- 二、PostgreSQL:每个城市保留姓名排序后的第一行
SELECT DISTINCT ON (city) city, name
FROM users
ORDER BY city, name;
-- 三、MySQL 等价改写:窗口函数打行号再筛
SELECT city, name
FROM (
SELECT city, name,
ROW_NUMBER() OVER (PARTITION BY city ORDER BY name) AS rn
FROM users
) t
WHERE rn = 1;
-- 组合个数统计:子查询形式两库通用
SELECT COUNT(*) AS combos FROM (SELECT DISTINCT city, status FROM users) x;写法语义对照
| 写法 | 语义 | 注意事项 |
|---|---|---|
| DISTINCT 多列 | 按列组合去重 | 作用于整个清单,不是只对第一列去重 |
| DISTINCT ON | 每个分组保留一行 | 表达式必须是排序条件的最左前缀 |
| 窗口函数取行号 | 每个分组保留一行,可移植 | 写法更长,但两库行为一致 |
| 分组后取裸列 | 看似能取一行,实际不确定 | 严格模式下直接报错,不要采用 |
「每组取最新一行」的正确做法是窗口函数或 DISTINCT ON。写成按分组列聚合、再直接选出未参与聚合的列,属于结果不确定的写法:同一 SQL 在不同时间可能返回不同行,排查线上数据不一致时很难定位。
去重后紧跟分页要留意代价:数据库必须先拿到完整去重结果才能排序取前若干行,大表上非常慢。如果业务允许,先在业务键上加唯一约束,从写入侧治理重复,比查询侧去重更省资源。