🐬 第16章 创建高级联结
📄 一句话概括

上一章的联结只用了两张表、一种写法。 这一章补齐三件工具:表别名、自联结、外联结, 外加一个最容易写错的搭配——聚集函数配外联结。 Demo 里自联结和子查询给出同样的六行;内联结丢掉没下单的 Mouse House,左外联结把它捞回来; COUNT(列) 给 0 而 COUNT(*) 给 1——外联结造的那行全是 NULL。

📝 本章和前面几章的关系

第 15 章的等值联结只留「两边都对上」的行, 这一章讲对不上的怎么办——那正是业务里最常问的问题(谁没买过、哪个产品没卖过)。 外联结是全书最容易被误用的一处:条件写在 ON 还是 WHERE,结果完全不同。

一、表别名:让同一张表出现两次

FROM vendors AS v 给表起了个短名,之后整句都用 v. 前缀。 别名只在这一次查询里有效,不改表名、不影响别的查询。 好处有三个:少打字、同名列不打架、最重要的是同一张表能出现两次。 这一点和列别名不同:列别名改的是表头,表别名改的是「这句话里怎么称呼它」。 有些数据库(比如 Oracle)要求表别名不带 AS,MySQL 和 SQLite 两种都认。

二、自联结:自己跟自己连

自联结就是把一张表当两张用:products AS p1, products AS p2。 两个别名必须不同,否则数据库分不清你说哪一份。 教材那个例子:「找出与 DTNTR 同一供应商的所有产品」。 p2 负责查出 DTNTR 的 vend_id,p1 负责列出这个 vend_id 下的产品。 同样的问题用子查询也能做,而且更像人话: WHERE vend_id = (SELECT vend_id FROM products WHERE prod_id = 'DTNTR')。 教材的结论是自联结通常更快,因为子查询可能被当成两步执行。 先写对,再谈快——两种写法结果一样时,选你看得懂的那个。

三、不同类型的联结:内、左外、右外、全外

内联结(INNER JOIN)只留两边都对上的行。 客户和订单内联结, 没下过单的客户直接消失——他甚至不会告诉你「有个人没下单」。 左外联结(LEFT OUTER JOIN)保留左表全部行,右边配不上就填 NULL。 这是「以左表为主」的写法:五个客户必须都出现,下过单的带单号,没下过的是 NULL。 右外联结反过来保留右表全部行,全外联结两边都保留。 注意支持情况:老版本 SQLite 只有 LEFT,MySQL 没有 FULL JOIN(要用 LEFT 加 UNION 拼), 本课这套引擎三个都支持。 一个必须记住的坑:条件写在 ON 和写在 WHERE 效果不同。 写在 ON 是「怎么连」,写在 WHERE 是「连完再筛」——把 o.order_num IS NULL 写进 ON, 左外联结就退化成内联结了。

四、外联结的经典用法:找「没有」

「哪些客户一单没下」这个问题,内联结永远答不出来,因为那些客户根本不在结果里。 标准解法:左外联结 + WHERE 右表主键 IS NULL。配不上的行才有 NULL,恰好就是你要找的缺口。 这个套路可以套在很多问题上:没卖过的产品、没写过评论的用户、 没交过作业的学生——凡是「找出没做过某事的人」,都是这一句。

五、带聚集函数的联结

「每个客户下了几单」需要联结加分组:左外联结保住全部客户,再 GROUP BY 客户 数单号。 这里有个致命细节:数什么。 COUNT(o.order_num) 数非 NULL,没下单的客户给 0; COUNT(*) 数行,外联结给他造了一行,于是给 1。 教材专门把这两个摆在一起对比,因为 COUNT(*) 那个 1 看起来完全正常, 只有跟业务对账时才会发现「这个客户明明没买过」。

六、小结

教材小结:表别名可自联结,联结分内与外,外联结用 LEFT OUTER JOIN,配聚集函数时要数对列。 本课补三条:一是自联结与子查询等价,通常自联结更快;二是「找没有」靠左外联结加 IS NULL; 三是外联结加 COUNT 要用列名,星号会把空行数成 1。

七、动手 Demo:别名、自联结、三种外联结一次看完

Demo 七段:先给表起别名,再用自联结查同门产品,接着对比子查询写法; 然后内联结与外联结摆一起看「丢了几行」,最后用聚集函数踩一次 COUNT 的坑。 重点盯三处行数:内联结 5、左外联结 6、全联结 6——差的那一行就是 Mouse House。

import sqlite3
con = sqlite3.connect(":memory:")

# 表别名:v 就是 vendors,p 就是 products
con.execute("SELECT v.vend_name, p.prod_name FROM vendors AS v, products AS p "
            "WHERE v.vend_id = p.vend_id ORDER BY v.vend_name")
# 自联结:同一张表两个别名,找与 DTNTR 同供应商的其他产品
con.execute("SELECT p1.prod_id, p1.prod_name FROM products AS p1, "
            "products AS p2 WHERE p1.vend_id = p2.vend_id "
            "AND p2.prod_id = 'DTNTR'")
# 同一件事的子查询写法
con.execute("SELECT prod_id, prod_name FROM products WHERE vend_id = "
            "(SELECT vend_id FROM products WHERE prod_id = 'DTNTR')")
# 内联结漏掉没下单的客户,左外联结补回来
con.execute("SELECT c.cust_id, o.order_num FROM customers AS c "
            "INNER JOIN orders AS o ON c.cust_id = o.cust_id")
con.execute("SELECT c.cust_id, o.order_num FROM customers AS c "
            "LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id")
# 找「没有」:配不上的行 IS NULL
con.execute("SELECT c.cust_name FROM customers AS c "
            "LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id "
            "WHERE o.order_num IS NULL")
# COUNT(列) 不数 NULL,COUNT(*) 会把它数成 1
con.execute("SELECT c.cust_name, COUNT(o.order_num) AS n FROM customers AS c "
            "LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id "
            "GROUP BY c.cust_id")

实跑输出:

--------------------------------------------------------------------
第十六课:创建高级联结
--------------------------------------------------------------------

  【一】表别名:给表起个短名
  SQL> SELECT v.vend_name, p.prod_name FROM vendors AS v,
       products AS p WHERE v.vend_id = p.vend_id ORDER BY v.vend_name, p.prod_name
  vend_name       prod_name
  -------------------------------
  ACME            Bird seed
  ACME            Carrots
  ACME            Detonator
  ACME            Sling
  ACME            TNT (1 stick)
  ACME            TNT (5 sticks)
  Anvils R Us     .5 ton anvil
  Anvils R Us     1 ton anvil
  Anvils R Us     2 ton anvil
  Anvils R Us     Safe
  Jet Set         JetPack 2000
  LT Supplies     Fuses
  LT Supplies     Oil can
  (13 行)
  **AS 给表起别名**,`v` 就是 vendors、`p` 就是 products。 结果和上一章第 15 章那段完全一样。
  **别名只在这一次查询里有效**,不改表名、不影响别的查询。
  **好处有三**:少打字、让同名列不打架、**最重要的——同一张表可以出现两次。**

  【二】自联结:一张表当两张用
  SQL> SELECT p1.prod_id, p1.prod_name FROM products AS p1,
       products AS p2 WHERE p1.vend_id = p2.vend_id AND p2.prod_id = 'DTNTR'
  prod_id     prod_name
  ---------------------------
  DTNTR       Detonator
  FB          Bird seed
  FC          Carrots
  SLING       Sling
  TNT1        TNT (1 stick)
  TNT2        TNT (5 sticks)
  (6 行)
  **p1 与 p2 是同一张表**:p2 用来定位「DTNTR 的供应商是谁」,p1 用来列出这个供应商的所有产品。
  **六个,都是 1003 的。**
  **自联结的必须条件:两个别名必须不同**,否则数据库分不清你说哪一份。

  【三】同一个问题,子查询也能做
  SQL> SELECT prod_id,
       prod_name FROM products WHERE vend_id = (SELECT vend_id FROM products WHERE prod_id = 'DTNTR')
  prod_id     prod_name
  ---------------------------
  DTNTR       Detonator
  FB          Bird seed
  FC          Carrots
  SLING       Sling
  TNT1        TNT (1 stick)
  TNT2        TNT (5 sticks)
  (6 行)
  **也是六行,结果一模一样。** 这是第 14 章那条子查询路。
  **教材的结论是:自联结通常更快。** 因为子查询可能被当成两步执行,而自联结一次扫完。
  **不过可读性上子查询更直观**,先写对再谈快。

  【四】外联结:左表一行都不许丢
  SQL> SELECT c.cust_id,
       o.order_num FROM customers AS c INNER JOIN orders AS o ON c.cust_id = o.cust_id ORDER BY c.cust_id
  cust_id     order_num
  ----------------------
  10001       20005
  10001       20009
  10003       20006
  10004       20007
  10005       20008
  (5 行)
  **内联结给五行。** 五个客户里只有四个下过单,**Mouse House 一单没下,被内联结丢了。**
  SQL> SELECT c.cust_id,
       o.order_num FROM customers AS c LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id ORDER BY c.cust_id
  cust_id     order_num
  ----------------------
  10001       20005
  10001       20009
  10002       NULL
  10003       20006
  10004       20007
  10005       20008
  (6 行)
  **外联结给六行。** 10001 有两单占两行,Mouse House 那行的 order_num 是 NULL。
  **LEFT 的意思是「左边的表全留」**,右边配不上的位置填 NULL。
  **教材建议写全 `LEFT OUTER JOIN`**,虽然 `LEFT JOIN` 等价,写全更能说明意图。

  【五】外联结的经典用法:找「没有」
  SQL> SELECT c.cust_id,
       c.cust_name FROM customers AS c LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id WHERE o.order_num IS NULL
  cust_id     cust_name
  ------------------------
  10002       Mouse House
  (1 行)
  **找出没下过单的客户,就靠 `IS NULL`。** 一行:Mouse House。
  **这类「找缺口」的查询内联结做不到**——因为配不上的行根本不出现,你也就看不见缺口。

  【六】右联结与全联结
  SQL> SELECT COUNT(*) AS n FROM customers AS c RIGHT JOIN orders AS o ON c.cust_id = o.cust_id
  n
  --
  5
  (1 行)
  SQL> SELECT COUNT(*) AS n FROM customers AS c FULL JOIN orders AS o ON c.cust_id = o.cust_id
  n
  --
  6
  (1 行)
  **右联结反过来「右边的表全留」**,这里是 5 行,因为五张单的客户都找得到。
  **全联结两边都留**,6 行:五张单加一个没下单的客户。
  **老版本 SQLite 只有 LEFT,右联结要把两张表调个位置**;
  **MySQL 也不支持 FULL JOIN**,得用 LEFT 加 UNION 拼。这一课这套引擎三个都支持。

  【七】带聚集函数的联结:COUNT 星号会骗人
  SQL> SELECT c.cust_name,
       COUNT(o.order_num) AS num_ord FROM customers AS c LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id GROUP BY c.cust_id ORDER BY num_ord DESC
  cust_name          num_ord
  -----------------------------
  Coyote Inc.        2
  Wascals            1
  Yosemite Place     1
  E Fudd             1
  Mouse House        0
  (5 行)
  **Mouse House 给 0**,因为 `COUNT(列)` 不数 NULL,它配出来的那行 order_num 是 NULL。
  SQL> SELECT c.cust_name,
       COUNT(*) AS num_ord FROM customers AS c LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id GROUP BY c.cust_id ORDER BY num_ord DESC
  cust_name          num_ord
  -----------------------------
  Coyote Inc.        2
  Mouse House        1
  Wascals            1
  Yosemite Place     1
  E Fudd             1
  (5 行)
  **换成 `COUNT(*)`,Mouse House 变成 1。** 因为外联结确实给它造了一行,
  只是那行的值全是 NULL——**数行数和数有效值,是两件事。**

第一段别名。 结果和第 15 章逐行一致,只是写法短了。 第二段自联结。 六行,都是 1003 的产品——包括 DTNTR 自己。 第三段子查询。 同样六行,两条路都通,选你看得懂的。 第四段内外对比。 内联结 5 行,左外 6 行,多的那行 10002 的 order_num 是 NULL。 第五段找缺口。 IS NULL 一句,直接点出 Mouse House。 第六段右联结与全联结。 5 行与 6 行,右联结没多出行,因为每张单的客户都找得到。 第七段 COUNT 的坑。 先给 0,换成星号就变 1。两段结果只有 Mouse House 那一行不同。

30 秒实验

  1. 把第一段的 ORDER BY v.vend_name 去掉,13 行的顺序变不变?
  2. 用自联结找出「与 ANV01 同供应商的产品」,应该有几种?
  3. 把第七段的 COUNT(o.order_num) 换成 SUM(o.order_num IS NULL),结果说明什么?

常见误区

  1. 以为 LEFT JOIN 和 INNER JOIN 差不多。左外保留左表全部,内联结丢掉配不上的行。
  2. 把 WHERE 条件写成 ON 条件。写在 ON 里就成了联结条件,外联结会退化成内联结。
  3. 外联结配 COUNT(*)。配不上的行也算一行,没下单的客户会被数成 1。
自测三问(点开对答案) 1. 自联结和子查询做同一件事,怎么选? —— 结果一样时,通常自联结更快(一次扫描),子查询更好读;先写对,再按需要换。 2. 怎么找出「一次都没下单的客户」? —— `customers LEFT OUTER JOIN orders ON ... WHERE orders.order_num IS NULL`; 配不上的行才有 NULL,就是缺口。 3. 左外联结后 `COUNT(*)` 和 `COUNT(o.order_num)` 为什么不同? —— 星号数行,外联结为没下单的客户造了一行(值全 NULL),所以数成 1;列名只数非 NULL,给 0。

多表能拼、缺口能找到, 但一次查询只能有一种「行」的样子。 下一章把几条独立查询的结果上下摞起来——组合查询。

相关笔记