🦏 第3章 查询操作
📄 一句话概括

distinct 去的是「整行组合」的重复,不是某一列自己的重复。 这一点是初学者最容易搞错的地方。 Demo 里 DISTINCT class_id 给 3 行,DISTINCT class_id, gender 给 6 行, DISTINCT gender 给 2 行—— 同一个 distinct,三种结果,差别只在后面写了几列。 count(distinct 列) 还能数出「有几种」。

📝 本章和前面几章的关系

上一篇限制行数,这一篇处理重复。 它是查询四件套里最后一个。 它也是最容易掩盖问题的一个: 结果里出现不该有的重复,加 distinct 就「看不见」了,但根因还在。

一、单列去重:有几种不同的值

SELECT DISTINCT 列 FROM 表 返回这一列所有不重复的值,等价于「这一列有几种」。 DISTINCT 作用于 SELECT 后面的所有列, 所以写一列就是「这一列去重」,写两列就是「组合去重」。 DISTINCT 必须紧跟在 SELECT 后面,写成 SELECT 列, DISTINCT 列2 是语法错。

二、多列去重:去的是组合

DISTINCT class_id, gender 去的是「班级和性别都相同」的行。 三个班两种性别,组合出 6 行——正好是 3 × 2。 它不等于「每列各自去重」: 后者会得到 3 + 2 = 5 个值,而且是拼不回去的。 组合去重才是 SQL 的行为。 记法:distinct a, b = 「把 (a, b) 当成一个整体看有几种」。

三、数一数:count(distinct 列)

COUNT(DISTINCT 列) 直接给「有几种不同的值」,比先 distinct 再数少一步。 COUNT(*) 与 COUNT(DISTINCT 列) 放一起,才知道去重掉了多少:Demo 里 12 行、3 种班级。 COUNT(列) 与 COUNT(DISTINCT 列) 也不一样: 前者不数 NULL(见第 4 章),后者也不数 NULL。 两个都不数 NULL,这是它们的共同点。

四、distinct 和 group by 的关系

SELECT DISTINCT a, b FROM t 与 SELECT a, b FROM t GROUP BY a, b 结果一样—— 前者是后者的简写。 区别在「能不能带聚合」:distinct 只能去重、不能算; group by 后面可以跟 count()/sum()。 要算就用 group by。 两个叠在一起基本没用(Demo 第五段):group by 已经把每组压成一行,再去重也没东西可去。

五、distinct 的代价,以及它暴露的问题

代价:去重要排序或建哈希表,数据量大时明显慢;用不上索引时会走临时表。 更值得警惕的是它的「症状」作用: 查询结果里出现不该有的重复,正确做法是查「为什么重复」—— 多半是少写了 join 条件、或者一对多关系没处理。 加 distinct 把重复盖住,等于把问题藏起来: 数据一多,性能先垮;哪天去重盖不住了,结果就开始错。

六、小结

教材小结:distinct 用于去掉结果集中的重复行,可以作用于单列或多列组合。 本课补三条:一是它去的是「整行组合」的重复,不是单列各自去重; 二是 count(distinct 列) 直接数「有几种」;三是重复出现先查原因,别急着加 distinct。

七、动手 Demo:一个 distinct,三种结果

Demo 五段:单列去重、count(distinct)、多列组合去重、与 group by 叠加,最后讲代价。 第二段之后那个对比最值得看:distinct gender 给 2 行, distinct class_id, gender 给 6 行——同一个关键字,两种含义。

import _q46 as Q
c = Q.con()

c.execute("SELECT DISTINCT class_id FROM students ORDER BY class_id")
c.execute("SELECT COUNT(*) AS all_rows, "
          "COUNT(DISTINCT class_id) AS kinds FROM students")

# 多列:去的是「组合」重复
c.execute("SELECT DISTINCT class_id, gender FROM students "
          "ORDER BY class_id, gender")
c.execute("SELECT DISTINCT gender FROM students")

# 与 group by 叠加基本没用
c.execute("SELECT DISTINCT class_id, COUNT(*) AS n FROM students "
          "GROUP BY class_id")

实跑输出:

--------------------------------------------------------------------
第十四课:distinct,去的是「整行」不是「某一列」
--------------------------------------------------------------------

  单列去重:班级有几种
  SQL> SELECT DISTINCT class_id FROM students ORDER BY class_id
  class_id
  ---------
  1
  2
  3
  (3 行)

  **12 个学生只有 3 个班,去重后 3 行。**

  去重之后数一数:count(distinct ...)
  SQL> SELECT COUNT(*) AS all_rows, COUNT(DISTINCT class_id) AS kinds FROM students
  all_rows     kinds
  ----------------------
  12           3
  (1 行)

  **`count(*)` 数行,`count(distinct 列)` 数「有几种不同的值」。**
  **两个数放一起,才知道去重掉了多少。**

  多列去重:distinct 作用于整行组合
  SQL> SELECT DISTINCT class_id, gender FROM students ORDER BY class_id, gender
  class_id     gender
  ----------------------
  1            女
  1            男
  2            女
  2            男
  3            女
  3            男
  (6 行)

  **`distinct class_id, gender` 去掉的是「两个值都相同」的重复行。**
  **它不等于「每列各自去重」——那是两件事。**

  SQL> SELECT DISTINCT gender FROM students
  gender
  -------
  男
  女
  (2 行)

  **这一句给 2 行(男、女),但上一句给 6 行(班级与性别的组合)。**
  **差别就在「去重的是哪几列的组合」。**

  distinct 与聚合一起用:容易踩的一个坑
  SQL> SELECT DISTINCT class_id, COUNT(*) AS n FROM students GROUP BY class_id
  class_id     n
  ----------------------
  1            4
  2            4
  3            4
  (3 行)

  **这句里的 distinct 基本没用**:`group by` 已经把每个班合成一行了,再去重也没东西可去。
  **distinct 和 group by 常常二选一,别叠加。**

  distinct 的代价
  **去重要排序或建哈希表**,数据量大时明显慢。
  **更要紧的是**:
  **`select distinct` 常常是「查询写错了」的症状**——本来该用 `group by` 或该加 `where`,
  却用去重把重复行盖住。 **看到结果里有不该出现的重复,先去查为什么重复,别急着加 distinct。**

  **性能上**:`distinct` 用不上索引时会走临时表;
  **`group by` 在 8.0 里能吃到索引顺序**,同样的事优先用它。

第一段单列。 12 个学生 3 个班,去重后 3 行。 第二段两个数。 count(*) 给 12、 count(distinct class_id) 给 3——两个数放一起才知道去掉了 9 行。 第三段组合。 class_id, gender 给 6 行,正好 3 × 2。 第四段对照。 同一张表,distinct gender 只有 2 行——差别在于 distinct 作用于几列。 第五段叠加。 distinct class_id, count(*) 加 group by, distinct 白写——group by 已经压成一行了。

30 秒实验

  1. SELECT DISTINCT gender, class_id 与 SELECT DISTINCT class_id, gender 行数一样吗?
  2. (先想再跑)SELECT COUNT(DISTINCT gender) FROM students 给几?
  3. 把第五段的 distinct 去掉,结果会变吗?

常见误区

  1. 以为 distinct a, b 是「a 去重、b 去重」。 它是把 (a, b) 当整体去重,结果行数完全不同。
  2. 用 distinct 掩盖 join 写错。 一对多没处理干净时结果会翻倍,加 distinct 只是把症状盖住, 数据一多还是要爆。
  3. distinct 与 group by 叠加。 基本没用,还多一次去重开销。
自测三问(点开对答案) 1. `DISTINCT a, b` 去的是什么? —— 去掉「a 和 b 都相同」的重复行,也就是把 (a, b) 当成一个整体去重。 2. `COUNT(DISTINCT 列)` 与 `COUNT(列)` 有什么区别? —— 前者数「有几种不同的值」,后者数「非空的行数」;两者都不统计 NULL。 3. 结果里出现重复,第一反应该做什么? —— 查为什么重复(join 条件、一对多关系),而不是先加 distinct 把重复盖住。

查询四件套(select/where/order by/limit/distinct)齐了, 下一篇开始算数:sum/avg/max/min/count,以及分组。

相关笔记