🛢️ 第9章 数据查询
📄 一句话概括

连接只有一件事:按条件把两张表的行配对,配不上的怎么办就是内外连接的区别。 漏写连接条件不会报错,它给你一份「看着有数据、其实全是重复」的结果。 左连接和右连接的差别只是「保留哪一边」,写哪个都行,习惯上只用左连接。

📝 本章和前面几章的关系

9.4 讲三件事:内连接、外连接、复合条件连接。前两件是重点,第三件只是「条件里多写几句」。 这一篇的「漏写条件」,和第 9.5 篇的 NOT IN 空值陷阱是一对: 都是「不报错但结果全错」的典型。

一、交叉连接:每一行配每一行

不给连接条件时,数据库把两张表的所有组合都给你,这叫笛卡尔积。 行数是两张表行数相乘,11 行配 5000 行就是 55000 行。 这个数看着不大,但它是「相乘」不是「相加」,所以规模一大就失控。 换成两张十万行的表,结果是一亿行——这不是慢,是直接跑不出来。 所以看到「结果行数比预想多几十倍」,第一个怀疑对象就是漏了连接条件。

二、内连接与外连接

内连接是「交叉连接 + 条件筛选」,只留配得上的组合。 外连接是「不管配不配得上,先把某一边全留下」,另一边配不上就补 NULL。

连接方式 保留哪一边 结果行数
内连接 只留配得上的 4865
左外连接 保留 5000 个商品 5000
右外连接 保留 11 个类别 4866

左连接比内连接多出来的 135 行,就是「有商品但没类别」的那些。 右连接比内连接多 1 行,多的是那个「新品类」,它没有任何商品。 右连接比左连接少,不是因为哪个更强,是因为两边保留的东西不一样。 左连接保留「每一个商品」,右连接保留「每一个类别」,两张表行数本来就不同。 实践里几乎只用左连接,因为它读起来顺着「主表在左边」的思路走。 右连接除了让人多看两眼,没有别的作用——把两张表位置对调就是左连接。 外连接还有一个反直觉的用法:LEFT JOIN ... WHERE 右表.主键 IS NULL 用来找孤儿数据。 这一招在清洗数据时非常常用,比写子查询快得多。

三、复合条件连接与自连接

复合条件连接就是 ON 后面多写几个 AND,语法上没有新东西。 它的价值在于「连接之外还要筛」,把条件都放在 ON 里比放 WHERE 更清楚。 自连接是「同一张表取两个别名互相连」,典型场景是树形结构:员工和上级。 这里必须用 LEFT JOIN,否则没有上级的那个「总经理」会消失。 自连接必须给两个别名,这不是风格问题,是语法要求。 不起别名时,引擎不知道 name 是员工的名字还是上级的名字,直接报「列名有歧义」。 代价上要留意:自连接是「同一张表扫两遍」。 十万行的表自连接,等于二十万行的读,估算时要乘二。

四、忘写连接条件会怎样

这一节是这一篇最重要的地方。 把连接条件漏掉,SQL 不报错、不警告,它给你笛卡尔积的一个子集。 这次的结果是 2386 行变成 26983 行,多了十一倍多。 拿它去 SUM 金额,金额会被放大同样的倍数。 最要命的是「看着差不多」:数出来了,还像个正常数字,往往过很久才被发现。 预防办法只有一个:连接一律用 JOIN ... ON 的写法,不用逗号加 WHERE。 因为 ON 是必填的,漏了直接语法错;而逗号写法漏了条件,语法上完全合法。 让语法帮你挡住错误,比让自己记住更可靠。

五、动手 Demo:五种连接,和忘写条件的那一场灾难

两张表:tb_goods 5000 行(其中 135 行没类别), tb_cat2 11 行(10 个正常类别 + 1 个没有任何商品的「新品类」)。 这个组合能把内外连接的差别全暴露出来。

import sqlite3

c = sqlite3.connect(u"d039.db")

# ① 交叉连接
print(c.execute(u"SELECT COUNT(*) FROM tb_cat2, tb_goods").fetchone())

# ② 内连接 / 左外 / 右外
print(c.execute(u"SELECT COUNT(*) FROM tb_goods g "
                u"INNER JOIN tb_cat2 k ON g.cat_id = k.id").fetchone())
print(c.execute(u"SELECT COUNT(*) FROM tb_goods g "
                u"LEFT JOIN tb_cat2 k ON g.cat_id = k.id").fetchone())
print(c.execute(u"SELECT COUNT(*) FROM tb_goods g "
                u"RIGHT JOIN tb_cat2 k ON g.cat_id = k.id").fetchone())

# ③ 找孤儿数据
print(c.execute(u"SELECT COUNT(*) FROM tb_goods g "
                u"LEFT JOIN tb_cat2 k ON g.cat_id = k.id "
                u"WHERE k.id IS NULL").fetchone())

# ④ 漏写连接条件的后果
print(c.execute(u"SELECT COUNT(*) FROM tb_goods g, tb_cat2 k "
                u"WHERE g.price > 100").fetchone())          # 漏了
print(c.execute(u"SELECT COUNT(*) FROM tb_goods g, tb_cat2 k "
                u"WHERE g.price > 100 AND g.cat_id = k.id").fetchone())

实跑输出:

--------------------------------------------------------------------
第三十九课:五种连接,和忘写条件的那一场灾难
--------------------------------------------------------------------

  **教材 9.4 讲连接查询。连接的核心只有一句话:把两张表按某个条件配对。**
  **配不上的行怎么办,就是内连接和外连接的区别。**
  **这一课跑五种连接,再故意忘写一次连接条件,看会发生什么。**

  一、交叉连接:每一行配每一行
  --------------------------------------------------------------------

  表            行数
  -------------------
  `tb_cat2`     11
  `tb_goods`    5000
  两者交叉连接  55000
  **第三行等于前两行相乘。**
  **这就是笛卡尔积:不给条件,数据库就把所有组合都给出来。**
  **11 行乘 5000 行得 55000 行,看着不多;换成两张十万行的表,就是一亿行。**

  二、内连接:只要配得上的
  --------------------------------------------------------------------
  code   name      cat
  ---------------------
  G00001 数码商品1 数码
  G00002 家电商品2 家电
  G00003 服饰商品3 服饰
  3 rows in set
  **`INNER JOIN` 就是「交叉连接 + 条件筛选」,只留配得上的组合。**
  **这里配出 4865 行,比 55000 少了一个数量级,因为绝大多数的组合是错配。**

  三、左外连接:左边的全留,右边配不上就补空
  --------------------------------------------------------------------

  连接方式          结果行数  说明
  ----------------------------------------------------
  内连接            4865      配不上的都丢掉
  左外连接          5000      左表全留,右边补 NULL
  其中右表为空的行  135       这些就是「没类别」的商品
  **第二行比第一行多出来的 135 行,就是「有商品但没类别」的那些。**
  **所以外连接有一个反直觉的用法:`LEFT JOIN ... WHERE 右表.主键 IS NULL` 用来「找孤儿数据」。**
  **这一招在清洗数据时非常常用。**

  四、右外连接,其实就是换个方向的左连接
  --------------------------------------------------------------------

  连接方式  保留哪一边        结果行数
  ------------------------------------
  内连接    只留配得上的      4865
  左外连接  保留 5000 个商品  5000
  右外连接  保留 11 个类别    4866
  **`RIGHT JOIN` 的结果和把两张表位置对调的 `LEFT JOIN` 完全一样。**
  **这里给 4866 行,比内连接多 1 行——多的是那个「新品类」,它没有任何商品。**
  **但它比左连接少,因为两边保留的东西不一样。**
  **左连接保留「每一个商品」,右连接保留「每一个类别」。**
  **所以行数不同不是矛盾,是两张表行数本来就不同。**
  **实践里几乎只用 `LEFT JOIN`,因为它读起来顺着「主表在左边」的思路走。**
  **`RIGHT JOIN` 除了让人多看两眼,没有别的作用。**
  **`FULL OUTER JOIN` 是「两边都留」,MySQL 不支持,要用 `LEFT UNION RIGHT` 拼。**
  **所以跨库搬 SQL 时,看到 `FULL JOIN` 就知道要改。**

  五、忘写连接条件的后果
  --------------------------------------------------------------------

  写法          结果行数  耗时
  ---------------------------------
  写了连接条件  2386      876 微秒
  漏了连接条件  26983     1.80 毫秒
  **漏了条件之后,结果变成了原来的 11.3 倍。**
  **而且它不报错、不警告——你会拿到一份「看起来有数据、其实全是重复」的结果。**
  **这种错误在下游很致命:拿它去 `SUM` 金额,金额会被放大同样的倍数。**
  **而且因为数字「看着差不多」,往往过很久才被发现。**
  **预防办法只有一个:连接一律用 `JOIN ... ON` 的写法,不用逗号加 `WHERE`。**
  **因为 `ON` 是必填的,漏了直接语法错;而逗号写法漏了条件,语法上是合法的。**

  六、自连接:一张表当两张用
  --------------------------------------------------------------------
  员工     上级
  ---------------
  总经理   NULL
  研发总监 总经理
  销售总监 总经理
  财务总监 总经理
  4 rows in set
  **自连接就是「同一张表取两个别名,然后互相连」。**
  **这里 `e` 是员工,`m` 是上级,其实是同一张 `tb_emp`。**
  **用 `LEFT JOIN` 是为了让「总经理」那一行也留下来——他没有上级,右边补 NULL。**
  **换成 `INNER JOIN`,总经理会消失,这往往不是你要的结果。**
  **自连接必须用表别名,否则没法区分「这一列是员工的还是上级的」。**
  **这是别名唯一「不写就报错」的场景,第 9.7 节会再讲。**
  **这张表只有 10 行,自连接只花了 107 微秒。**
  **但自连接是「同一张表扫两遍」,行数一大代价就翻倍。**
  **十万行的表自连接,等于二十万行的读。这一点在估算时容易漏。**

六个数排下来就是一条完整的推理线:55000、4865、5000、4866、135、26983。 55000 是相乘得到的,4865 是筛出来的,5000 和 4866 是「保留一边」得到的。 135 这个数最有用:它等于 5000 减 4865,也等于「左连接里右表为空的行数」。 两个算法给同一个数,说明「左连接多出来的行」和「孤儿数据」是同一批。 最后那两个数是 26983 和 2386,相差十一倍多。 注意 26983 不是整数倍关系——因为 price > 100 的商品在类别上分布不均。 这也说明「多了几倍」这种话不能凭猜,得真跑。 自连接那一段只有 10 行数据,耗时 63 微秒,看不出差别。 它的代价要等行数上来才显出来,这一点在估算时容易漏。

30 秒实验

  1. 把左连接换成内连接,看 135 行怎么消失的。
  2. 把 WHERE k.id IS NULL 去掉,看行数从 135 跳回 5000。
  3. 把自连接的 LEFT JOIN 换成 INNER JOIN,看总经理那一行怎么没了。

常见误区

  1. 用逗号加 WHERE 写连接,漏了条件,拿到十一倍的行还以为查对了。
  2. 以为右连接「更强」或者「结果更多」,其实它只是保留另一边。
  3. 自连接不起别名,或者以为别名可写可不写。
自测三问(点开对答案) 1. 内连接和外连接的区别,一句话怎么说? —— 内连接只留配得上的行;外连接先把某一边全留下,另一边配不上就补 NULL。 2. 为什么说右连接是多余的? —— 因为把两张表位置对调,右连接就变成左连接,结果完全一样。保留哪个写法只是阅读习惯问题。 3. 漏写连接条件为什么危险? —— 因为它在语法上完全合法,不报错不警告,只是结果行数被放大若干倍。 下游拿去求和,金额就跟着放大,而且往往很久才发现。

表拼完了,下一篇 9.5 讲把一条查询套进另一条:子查询,和 NOT IN 遇见空值就归零的陷阱。

相关笔记