查询里的查询:子查询

子查询是写在另一条查询中的查询。它可以提供一个值、一个待匹配的列表,或一张临时表。

返回一个值的子查询

凡是能写一个值的地方,都可以改写成一条只返回一行一列的查询。

1

SELECT name,
       price,
       ROUND((SELECT AVG(price) FROM products), 2) AS average_price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC
LIMIT 20;
在 Planche 中运行

结果 — 共 20 行,这里是前 5 行

namepriceaverage_price
Glide GPS Watch Pro2484.41230.21
Terra 3D Puzzle2481.771230.21
Aria Castle Kit2479.411230.21
Aria LED Strip Pro2463.411230.21
Nova Brief Answers2445.491230.21

浏览器会另开一个标签页打开 Planche,示例数据库已经载入,这条查询也已经放好。本页其他示例都会复用同一个标签页;在你按下 F9 之前,什么都不会执行。

(SELECT AVG(price) FROM products) 产生一个数字,查询的其余部分就当你直接把那个数字敲进去了一样。这也回答了"为什么不能写 WHERE price > AVG(price)"——聚合汇总的是一个组,而 WHERE 运行时还没有组。子查询绕开了这一点:它是一条自成一体的完整查询,在外层开始筛选之前就已经算完了。

返回一列值的子查询

2

SELECT first_name, last_name, email, country
FROM customers
WHERE id IN (
  SELECT customer_id
  FROM orders
  WHERE status = 'refunded'
)
ORDER BY last_name
LIMIT 20;
在 Planche 中运行

结果 — 共 20 行,这里是前 5 行

first_namelast_nameemailcountry
MayaAhmedmaya.ahmed1@mail.exampleUS
NoahAhmednoah.ahmed@inbox.exampleFR
LiamAhmedliam.ahmed1@mail.exampleDE
AishaAhmedaisha.ahmed@mail.exampleGB
SofiaAhmedsofia.ahmed@example.comES

内层查询给出一列客户 id,IN 判断是否在其中。从里往外读——"id 出现在退款订单的客户 id 之中的那些客户"。

EXISTS:只问有没有匹配上

3

SELECT c.first_name, c.last_name, c.country
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.total > 1000
)
ORDER BY c.last_name
LIMIT 20;
在 Planche 中运行

结果 — 共 20 行,这里是前 5 行

first_namelast_namecountry
AishaAhmedGB
AishaAhmedJP
AvaAhmedIT
EmmaAhmedIN
EthanAhmedFR

EXISTS 接一条查询,只要它至少返回一行就为真。写 SELECT 1 是惯例:没有人看被选出来的值,只看有没有行回来,所以选真实的列没有意义。

这条子查询引用了外层的 c.id,这使它成为相关子查询——它对每个客户都是一个不同的问题,而不是只算一次的一份列表。要问"有没有相关的行存在",这通常既是最清楚的写法,也是引擎优化得最好的写法。

NOT IN 的陷阱

120 名员工里有 110 人手下没人。这条查询就是要找他们:

4

-- Employees nobody reports to. 110 of the 120 qualify,
-- and this query finds none of them.
SELECT first_name, last_name, position
FROM employees
WHERE id NOT IN (SELECT manager_id FROM employees);
在 Planche 中运行

没有匹配的行

零行。也不报错。

manager_id 允许为空,组织架构最上面那十个人在这一列上是 NULL。于是内层查询返回的列表里含有一个 NULL,而 x NOT IN (1, 2, NULL) 问的是"x 是否既不等于 1,也不等于 2,也不等于这个未知的值?"。最后一段是未知,整个条件就是未知,于是没有任何一行会被留下——对每一个 x 都是如此。列表里任何位置只要有一个 NULL,结果就空了。

IN 不会这样被拖垮:只要某一行匹配上了一个真实的值,它就会被留下,列表里还有什么都无所谓。只有取反的形式要求所有比较都为真,所以一个未知就足以把结果清空。

解法要么是在子查询里加上 WHERE manager_id IS NOT NULL,要么——更好,因为不会忘——换成 NOT EXISTS:

5

SELECT e.first_name, e.last_name, e.position
FROM employees e
WHERE NOT EXISTS (
  SELECT 1
  FROM employees m
  WHERE m.manager_id = e.id
)
ORDER BY e.last_name
LIMIT 20;
在 Planche 中运行

结果 — 共 20 行,这里是前 5 行

first_namelast_nameposition
PriyaAhmedSenior Engineer
DiegoAhmedIntern
YukiAhmedEngineer
RaviBrownLead
JonasBrownSpecialist

NOT EXISTS 问的是行,不是值,所以 NULL 根本不会进入比较。默认就伸手去拿 NOT EXISTS,能一次性消掉一整类悄无声息的错误答案。

把子查询当表用

返回多行多列的查询,可以放在本该写表的位置,也就是 FROM 里。这样的查询叫派生表。

6

SELECT category, products, average
FROM (
  SELECT cat.name AS category,
         COUNT(*) AS products,
         ROUND(AVG(p.price), 2) AS average
  FROM products p
  JOIN categories cat ON cat.id = p.category_id
  GROUP BY cat.id
)
WHERE products >= 10
ORDER BY average DESC;
在 Planche 中运行

结果 — 共 26 行,这里是前 5 行

categoryproductsaverage
Wearables111672.29
Programming101585.67
Tablets171529.45
Audio111482.89
Kitchen121443.21

这是"我想按聚合值筛选"的另一个答案:在内层分组,再像对待普通表一样筛选结果。它干的活和 HAVING 一样,而且还能按别名筛选,这一点 HAVING 做不到可移植的版本。

派生表可以嵌套,而嵌套正是它变得难读的地方:套上三层,就只能从里往外读了。下一课会把它换成一种从上往下读的写法。

每行一条子查询

7

SELECT p.name,
       p.price,
       (SELECT COUNT(*) FROM reviews r WHERE r.product_id = p.id) AS reviews,
       (SELECT ROUND(AVG(r.rating), 2) FROM reviews r WHERE r.product_id = p.id) AS avg_rating
FROM products p
ORDER BY avg_rating DESC, reviews DESC
LIMIT 20;
在 Planche 中运行

结果 — 共 20 行,这里是前 5 行

namepricereviewsavg_rating
Atlas LED Strip1856.7525
Orbit Road Bike1731.2915
Onyx Sneakers187844.75
Brief Answers (3rd ed.)117.9664.67
Pulse Good to Great392.9734.67

现在你能做什么

除了把复杂查询写得好读,其他都能做了——而那正是下一课的内容。

自己动手写

  1. 哪些客户从来没写过评价?

    查看答案

    查看答案

    SELECT c.id, c.first_name, c.last_name
    FROM customers c
    WHERE NOT EXISTS (
      SELECT 1 FROM reviews r WHERE r.customer_id = c.id
    )
    ORDER BY c.id
    LIMIT 20;
    在 Planche 中运行

    结果 — 共 20 行,这里是前 5 行

    idfirst_namelast_name
    3YukiJang
    4InesMeyer
    12KenjiPark
    13JonasLarsen
    26AvaSilva

    用 NOT EXISTS,不用 NOT IN。这里 reviews.customer_id 恰好没有 NULL,两种写法都能出正确结果,但把 NOT EXISTS 变成习惯,就意味着真正出问题的那一次你本来就写对了。

  2. 库存高于平均水平的商品,多的排前面。

    查看答案

    查看答案

    SELECT name, stock_quantity
    FROM products
    WHERE stock_quantity > (SELECT AVG(stock_quantity) FROM products)
    ORDER BY stock_quantity DESC
    LIMIT 15;
    在 Planche 中运行

    结果 — 共 15 行,这里是前 5 行

    namestock_quantity
    Luma Phone Lite500
    Atlas Lens 35mm X499
    Sol Good to Great Plus498
    Echo Phone Pro Plus498
    Pulse Phone Pro Max497

    标量子查询把平均值算一次,外层查询拿每一行去比。凡是"高于/低于平均"的问题,记住这个形状就够了。