前面四篇,WHERE 后面跟的都是你自己写死的条件:amount > 100、city = '杭州'。条件里的数你事先知道。
现在换一个问题:哪些订单的金额高于所有订单的平均值?
这道题不一样了。条件里的那个数,你自己都不知道是多少,得先让数据库算出来。这一篇就讲这一件事:当筛选条件本身需要一个查询结果时,怎么把两条查询串成一条。
先把数据摆出来
orders 表还是前面那张,9 行:
| order_id | user_id | city | amount |
|---|---|---|---|
| 1001 | 8823 | 杭州 | 129.00 |
| 1002 | 9107 | 上海 | 58.50 |
| 1003 | 8823 | 杭州 | 320.00 |
| 1004 | 9310 | NULL | 76.00 |
| 1005 | 9107 | 杭州 | 45.00 |
| 1006 | 9107 | 上海 | 210.00 |
| 1007 | 9310 | 上海 | 91.50 |
| 1008 | 8823 | NULL | 19.90 |
| 1009 | 9500 | 北京 | 199.00 |
先手算一遍,后面才好核对:九笔加起来是 1148.90,除以 9,平均值 。所以“高于平均值”的应该是 129.00、320.00、210.00、199.00 这四笔。
卡住的地方:条件里的数要先算
最直接的想法是分两步:先单独跑一条 SELECT AVG(amount) FROM orders; 拿到 127.66,再把它填进条件 WHERE amount > 127.66。结果确实对,但这个做法站不住脚——明天新增一笔订单,平均值就不是 127.66 了,条件立刻过期;每换一个时间范围,你都得再手工抄一遍。
你想的是:能不能让数据库自己先算平均值,再拿这个结果去筛?答案是可以,但写法有讲究。
为什么不能直接写成 WHERE amount > AVG(amount)
很多人第一次会这么写,然后收到一条报错:
1SELECT order_id, city, amount
2FROM orders
3WHERE amount > AVG(amount);
4
5-- ERROR 1111 (HY000): Invalid use of group function“Invalid use of group function” 的意思是:聚合函数用错了地方 1。要理解为什么错,得回想一条查询内部的执行顺序:
关键在这两段的先后:WHERE 是逐行判断,它跑的时候聚合还根本没开始算。 数据库手里只有一行行的订单,没有“一组数”,AVG(amount) 无从下手。这跟第三篇里 HAVING 能放聚合函数是同一件事的两面——HAVING 站在聚合之后,WHERE 站在聚合之前。顺带说一句,这里的 HAVING 也帮不上忙:它筛的是“组”,而这道题要留下的是单笔订单。
办法:把算平均值的那条语句放进括号
真正要用的是这个写法:
1SELECT order_id, city, amount
2FROM orders
3WHERE amount > (SELECT AVG(amount) FROM orders);括号里的 (SELECT AVG(amount) FROM orders) 就是一个 子查询:一条完整的 SELECT,被当成一个值嵌进另一条语句里。
它的执行方式和你想的一致:括号里那条先独立跑一次,得到一个数 127.66;然后外层拿这个 127.66 当成普通数字,逐行判断 amount > 127.66。所以结果和手算一致:
| order_id | city | amount |
|---|---|---|
| 1001 | 杭州 | 129.00 |
| 1003 | 杭州 | 320.00 |
| 1006 | 上海 | 210.00 |
| 1009 | 北京 | 199.00 |
1001 的 129.00 只比 127.66 高一点点,照样进;1007 的 91.50 离平均线还差一截,进不来。这类“刚好跨过线”的行,是检验条件写对没写对的最好样本。
“标量”是什么意思,以及它带来的一个限制
括号里那种只返回 一行一列、也就是一个格子 的子查询,叫 标量子查询。“标量”就是“单个值”的意思,和你熟悉的 127.66、'杭州' 是同一类东西,区别只是这个值由查询算出来。
为什么值得专门给它一个名字?因为它能放的位置,取决于它返回的形状。amount > (...) 里,> 的右边只能站一个值。所以:
括号里那条查询只有在你确定它最多返回一行时,才能这样用。 如果它返回好几行,数据库不知道该拿哪一个去比较,直接报错:
1SELECT user_name FROM users
2WHERE user_id = (SELECT user_id FROM orders);
3
4-- ERROR 1242 (21000): Subquery returns more than 1 row这是最容易踩的一步。orders.user_id 有 9 行、8823 还重复三次,所以括号里吐出 9 行,= 右边站不住 2。
而 AVG(amount) 那种写法为什么稳?因为不带 GROUP BY 的聚合函数,无论表里有多少行,永远塌成一行。“聚合 + 没有 GROUP BY”是标量子查询最可靠的来源。
当括号里给出的是一份名单,就该用 IN
换一个问题:哪些用户下过单? 这次从 users 出发,条件是“这个 user_id 出现在 orders 的那一列里”:
1SELECT user_id, user_name FROM users
2WHERE user_id IN (SELECT user_id FROM orders);结果是三行:8823 阿May、9107 老周、9310 小林。Tony(9402)一单没下,不在名单里,所以不出现;9500 在名单里,但 users 里根本没有这个人,所以也不影响结果。
这里括号里返回的是 一列、九行——一份名单,不是单个值。IN 正好吃这个形状:它问的是“这个值在不在右边这一堆值里”,右边有几个都不妨碍。
一个细节值得留意:名单里 8823 重复了三次,结果并没有变成三行阿May。IN 只做“在不在”的判断,不会一对一把外层行复制出去。 这和上一篇的 JOIN 正好是鲜明对照:同样是“下过单的用户”,用 JOIN 拼回三笔订单就是三行,用 IN 仍然是一行。
判断该用哪种:只看括号里的形状
这两种写法不是“高级”和“低级”的区别,而是 形状匹配 的问题。写之前先在心里问一句:括号里那条查询会吐出什么?
<figure>养成一个习惯:拿不准时,先把括号里那条语句单独跑一次,看它到底返回几行几列。 这一步花三秒,能省掉大半的报错。
一个必须知道的地方:括号里的查询看不见外面的条件
这是子查询里最容易悄悄算错的地方。题目改成:杭州的订单里,哪些金额高于杭州的平均值?
杭州只有三笔:129.00、320.00、45.00,平均是 。所以正确答案只有 320.00 一笔。
如果你顺手写成这样:
1SELECT order_id, city, amount
2FROM orders
3WHERE city = '杭州'
4 AND amount > (SELECT AVG(amount) FROM orders);结果会是两行:129.00 和 320.00。多出来的 129.00 是错的。
原因就是前面说的:括号里那条查询是 独立跑的一遍,它面对的是整张 orders 表,算出来是 127.66 这个全表平均,它根本不知道外层还写了 city = '杭州'。要让平均值也只算杭州,必须在子查询自己的 WHERE 里再写一次条件:
1SELECT order_id, city, amount
2FROM orders
3WHERE city = '杭州'
4 AND amount > (SELECT AVG(amount) FROM orders WHERE city = '杭州');这样括号里算出 164.67,结果只剩 320.00 一笔。
把这件事记住成一句话:子查询是一个自足的查询,你在它里面没写的条件,它一律不认。 所以每次用子查询,都要追问一句:括号里那张表,应该被哪些条件限制?
现在你能回答什么
子查询要做的事只有一件:把一条查询的结果交给另一条查询用。写之前先确定括号里的查询会返回什么形状——一个值就放到比较运算符右边,一份名单就放 IN 的括号里;再检查括号里该有的筛选条件有没有漏。剩下的,就是多跑几次、拿手算的数对一对。
还有两件相邻的事这一篇没讲:括号里引用外层某一列的值(相关子查询),以及把子查询放进 FROM 里当一张临时表来用——后者是下一步把复杂问题拆成几步的关键,留到下一篇。