原来的题目很短:运营想知道用户来自哪些学校,从用户表里取出学校的去重数据。牛客原题:查询结果去重。
答案只有一行:
SELECT DISTINCT university FROM user_profile;但会写这行,不代表已经理解去重。比如再加一列用户编号,为什么同一所学校又出现了?这不是 DISTINCT 失效,而是我们改变了“什么算重复”。
1. 先看一张只有必要字段的表
下面是独立实验表,不需要使用或修改已有的 user_profile。请在自己的练习数据库中执行,表名已存在时换一个名字。
CREATE TABLE sql3_distinct_demo ( id INT NOT NULL PRIMARY KEY, university VARCHAR(32), province VARCHAR(32) NOT NULL ); INSERT INTO sql3_distinct_demo VALUES (1, '北京大学', '北京'), (2, '北京大学', '北京'), (3, '复旦大学', '上海'), (4, '复旦大学', '其他'), (5, NULL, '北京'), (6, NULL, '上海');| id | university | province |
|---|---|---|
| 1 | 北京大学 | 北京 |
| 2 | 北京大学 | 北京 |
| 3 | 复旦大学 | 上海 |
| 4 | 复旦大学 | 其他 |
| 5 | NULL | 北京 |
| 6 | NULL | 上海 |
为了观察 NULL,这里允许学校为空;原题学校列是 NOT NULL,原题答案并不依赖这些扩展行。
2. 单列去重:我们只关心学校
SELECT DISTINCT university FROM sql3_distinct_demo;结果集合是三项:北京大学、复旦大学、NULL。这里不列固定的先后顺序,因为 SQL 没有 ORDER BY 时不能依赖返回顺序。
可以把这个小例子想成:
先取出需要的列 再判断结果行是否重复 北京大学 北京大学 北京大学 复旦大学 复旦大学 -> 复旦大学 NULL NULL NULL这只是理解查询语义的画法,不代表数据库一定先物化整张中间表,再做一次扫描;实际执行可能借助索引等方式。
3. 多列去重:相同学校不再等于相同结果行
SELECT DISTINCT university, province FROM sql3_distinct_demo;结果集合变成五项:
| university | province |
|---|---|
| 北京大学 | 北京 |
| 复旦大学 | 上海 |
| 复旦大学 | 其他 |
| NULL | 北京 |
| NULL | 上海 |
第 1、2 行的两列都相同,合成一项。第 3、4 行学校一样,但省份不同,所以必须保留两项。
DISTINCT 不是只修饰紧跟在它后面的第一列,而是对整个选出的结果行去重。MySQL 官方也给出了多列 DISTINCT 与按这些列 GROUP BY 的对应例子。官方说明。
所以这句也不会得到“一所学校只留一个用户”:
SELECT DISTINCT id, university FROM sql3_distinct_demo;它仍然返回六行:id 每行不同,整个二元组就没有重复。
4. 去重不是删除,也不是挑一个代表
上面的查询不会删掉表里的第 2 行,也不会把第 4 行的学校改掉。
SELECT COUNT(*) AS original_rows FROM sql3_distinct_demo;仍是 6。DISTINCT 改的是结果集合,不是原始数据。
如果需求是“一所学校挑一个用户”,还必须说明挑谁:最早注册的?编号最小的?最近活跃的?只写 DISTINCT id, university 无法表达这些规则。原题只是取学校名单,不需要引入这个额外问题。
5. NULL 与排序:两个小边界
去重时,两项 NULL 可以被合并;这不意味着 WHERE university = NULL 能筛出空学校。筛空值仍要用 IS NULL。MySQL 的 NULL 说明。
如果业务只想要已知学校,并且要一个明确的排序:
SELECT DISTINCT university FROM sql3_distinct_demo WHERE university IS NOT NULL ORDER BY university;这里 DISTINCT 负责去重,WHERE 负责筛选,ORDER BY 负责排序。中文具体先后受排序规则影响,不能把“拼音排序”当默认承诺。
字符是否相等也受 MySQL 的 collation 影响。本文用完全相同的中文字符串验证重复,没有用 SQLite 的结果替代 MySQL 大小写、重音或尾空格行为。
6. 我怎么验证这些解释
本次在本地 SQLite 内存数据库执行上面的实验 SQL,校验以下结果;MySQL 语义另按官方文档核对。没有把它写成已经运行 MySQL 的截图,也没有测性能。
| 查询 | 校验目标 |
|---|---|
| 单列 DISTINCT | 3 项,NULL 只出现一次 |
| 两列 DISTINCT | 5 项,不误删不同省份的复旦大学 |
| id + 学校 DISTINCT | 6 项 |
| 原表 COUNT(*) | 6 行,查询没有删除记录 |
| 排除 NULL 后去重 | 2 所已知学校 |
另外验证了空表、全部重复和全部 NULL。无排序的查询按集合核对,不把某次碰巧返回的顺序写进断言。
写 SQL 前,我现在会先问一句:我要去重的对象,到底是一个字段的值,还是几列组成的一整行?这句话比记住 DISTINCT 的拼写更有用。