
我前阵子帮人收拾一个订单统计页面翻出SQL一看差点没绷住——一个报表查询里硬塞了五六个子查询每个都要把整张订单表扫一遍页面响应直接干到两秒多。其实这种需求压根不用那么复杂两张表JOIN一次再加个GROUP BY把聚合函数一用一条SQL就能出来。今天就把MySQL里聚合查询和联合查询这两块从头到尾捋一遍把函数用法、GROUP BY的坑、JOIN的理解、以及聚合和连表怎么配合全部讲透。这两块东西是SQL里最常用的硬核技能任何做后端、做数据分析的人每天都躲不开。文章里我会用实际业务场景来拆解比如统计订单、汇总用户消费、查商品分类销量这类例子配合能直接跑起来的SQL语句和踩坑记录让看完的人能立刻用到项目里。1. 聚合查询基础聚合函数到底在做什么1.1 聚合函数就是把一堆行压缩成一行聚合查询的核心说白了就一句话把多行数据按照某种规则合并计算最后得到一个结果。MySQL内置的聚合函数就那几个COUNT、SUM、AVG、MAX、MIN但越基础的东西越容易用错我先说几个很多人没真正想明白的细节。CREATE TABLE的示例结构我后面统一用这套后面写SQL都基于这三张表-- 用户表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT, city VARCHAR(50) ); -- 订单表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL ); -- 订单明细表 CREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL );1.2 COUNT系列最容易忽略NULL的坑COUNT函数的细节很多人根本不在意等到数据对不上了才回头看。COUNT(*)和COUNT(列名)的结果可能不一样原因就是NULL。COUNT(*)数的是行数一行只要存在就算而COUNT(列名)数的是这个列“非NULL”的个数。举个例子SELECT COUNT(*), COUNT(name), COUNT(age) FROM users;如果users表里有10个人但其中2个人的age字段没填NULL那结果就会是10、10、8。很多人平时用COUNT(1)代替COUNT(*)其实这俩没本质区别引擎层面基本都优化掉了个人习惯用COUNT(*)语义就是“数行数”。COUNT还有个进阶用法是COUNT(DISTINCT 列)比如统计有多少个不同的城市有用户下单一条SQL就搞定SELECT COUNT(DISTINCT city) FROM users;这里有个小提醒COUNT(DISTINCT)在数据量大的时候是相对昂贵的操作因为它要做去重排序不是简单数数后面做大报表的时候要注意这个性能点。1.3 SUM、AVG、MAX、MIN的语义细节SUM就是求和只对数值类型有意义这个没啥好说的。AVG是平均值但要注意它也是忽略NULL的不是拿NULL当0算。如果一组数据是10、20、NULLAVG结果是15而不是10。理解这个规则很重要否则你算用户平均消费时没消费的NULL记录会被直接跳过而不是被当成0拉低平均值。MAX和MIN不但能用在数字上字符串和时间类型也能用比如查最晚的下单时间直接用MAX(created_at)查最早的用户名按字典序就是MIN(name)。这俩用在索引列上的时候MySQL能直接走索引拿到第一或最后一条效率很高。实战里经常还要配合IFNULL或者COALESCE来处理聚合结果为NULL的情况。比如统计某个分类没有订单时SUM(amount)返回的就是NULL而不是0前端一旦直接显示就变成空白后端一般都要先兜底SELECT category_id, IFNULL(SUM(amount), 0) AS total_amount FROM products GROUP BY category_id;2. GROUP BY分组统计的核心逻辑2.1 GROUP BY是聚合查询的灵魂聚合函数单独用就只能把整张表压成一行实际业务根本不够用。比如“统计每个城市的用户数”如果只靠聚合函数你得写好几条SQL分别查但有了GROUP BY一条SQL就搞定SELECT city, COUNT(*) AS user_count FROM users GROUP BY city;这条SQL的执行逻辑可以这样理解先把users表按city分成若干组每个城市一组然后对每一组各自执行COUNT(*)每组输出一行结果。GROUP BY之后每组的行数可能大于1但SELECT里只能出现分组列或者聚合函数的结果因为其他列在这一组里有多个值MySQL没办法确定要显示哪一个这就是语义上为什么GROUP BY要和其他列隔离。这个限制很多人第一课就学过但真写起来还是会犯。我见过最多的一种错误写法是想在分组查询里拿到组内某一条记录的其他字段直接写-- 错误示例ONLY_FULL_GROUP_BY模式下直接报错 SELECT user_id, MAX(amount), order_id FROM orders GROUP BY user_id;这里order_id就不属于分组列也不是聚合函数MySQL直接抛错。如果你想拿每个用户金额最大订单的order_id正确思路是用子查询先把每个用户最大金额算出来再回表关联SELECT o.* FROM orders o INNER JOIN ( SELECT user_id, MAX(amount) AS max_amount FROM orders GROUP BY user_id ) t ON o.user_id t.user_id AND o.amount t.max_amount;这条SQL在主键索引合理的情况下性能也不错而且思路清晰面试也比较常考。2.2 星号规则ONLY_FULL_GROUP_BY到底在较什么劲MySQL 5.7之后默认开启了sql_mode里的ONLY_FULL_GROUP_BY意思是SELECT后面的非聚合列必须严格出现在GROUP BY子句里。这样做是为了避免歧义防止取到组内随机一条记录导致结果不稳定。很多人报错时看到的就是这句Expression #N of SELECT list is not in GROUP BY clause and contains nonaggregated column ...有些老项目是从5.5、5.6升级过来的之前没这个限制开着开着突然报错一群人抓瞎。排查方法很简单先看看当前sql_modeSELECT sql_mode;如果里面有ONLY_FULL_GROUP_BY就有两种处理方式。一种是修改sql_mode把它去掉但不建议因为这种“宽松模式”会让SQL结果不可预测哪天数据一变查询结果就漂了。另一种更靠谱就是老老实实把SQL改规范SELECT里的非聚合列都放进GROUP BY或者改成聚合函数包起来。实际开发里还有一种绕的办法是使用ANY_VALUE()函数表示“这一组里随便取一个”比如你想按用户分组但还想顺带看这个用户最早一笔订单的备注就可以写成ANY_VALUE(remark)。这是MySQL给特定场景留的后门别滥用但确实有它存在的价值。2.3 WHERE和HAVING过滤时机完全不同WHERE是在分组之前过滤原始行HAVING是在分组之后过滤组结果。这俩的操作时机不一样能用WHERE就用WHERE因为先过滤掉无关数据分组时的数据量更小性能更好。HAVING只用来做组级别的过滤条件。举两个例子对比一下-- 先过滤已取消的订单再按用户分组 SELECT user_id, COUNT(*) AS valid_order_count FROM orders WHERE status ! 2 GROUP BY user_id; -- 分组后只保留订单数大于等于3的用户 SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id HAVING COUNT(*) 3;这里有个新手常踩的坑想过滤“订单金额大于100”的用户写成WHERE SUM(amount) 100直接报错。聚合函数不能出现在WHERE里必须放HAVINGSELECT user_id, SUM(amount) AS total_spent FROM orders GROUP BY user_id HAVING SUM(amount) 100;2.4 GROUP BY加排序ORDER BY的正确姿势分组结果默认是不保证顺序的所以业务需要明确的顺序时一定要ORDER BY。最常见的写法就是按聚合结果倒序比如找消费金额最高的前10个用户SELECT user_id, SUM(amount) AS total_spent FROM orders GROUP BY user_id ORDER BY total_spent DESC LIMIT 10;MySQL 5.7之后还支持在GROUP BY里直接指定升降序比如GROUP BY city DESC但这种写法比较冷门而且未来版本已经在标记废弃建议统一用ORDER BY控制排序可读性和兼容性都更好。GROUP BY还有一个注意点如果按字符串分组排序默认按字典序不是按长度。想按长度排序需要在ORDER BY里写ORDER BY LENGTH(city)很多人会忘记这一点。3. 多表联合查询JOIN的底层逻辑3.1 为什么需要多表查询实际项目里几乎不会把所有字段塞在一张表里合理的数据库设计会把不同业务实体拆成多张表用外键关联起来。比如用户表单独一张订单表单独一张订单明细单独一张。查询“某个用户买了什么商品”的时候数据分散在users、orders、order_items三张表里不可能不连表就能查出来。多表查询的底层逻辑就是做笛卡尔积再加条件过滤。两张表JOIN理论上先把两张表的每一行互相组合生成一个大集合然后通过ON条件把不匹配的行剔除。MySQL优化器不会真的去生成那个庞大的笛卡尔积它会根据索引、统计信息选择最优的执行计划但理解这个概念对分析问题很有帮助——JOIN的结果行数变化本质上就是集合关系的变化。3.2 内连接INNER JOIN两边都对得上才留内连接的核心语义就是取两张表的交集只保留满足ON条件的数据。比如查所有下过单的用户信息SELECT u.id, u.name, o.id AS order_id, o.amount FROM users u INNER JOIN orders o ON u.id o.user_id;这条SQL的意思是user表里没下过单的人不会出现在结果里orders表里找不到对应用户的脏数据也不会出现。实际场景里由于外键约束的存在脏数据一般是被数据库挡住的但业务逻辑上“没匹配上”的记录确实会被过滤掉。内连接还有一种老式写法直接在FROM后加逗号在WHERE里写关联条件SELECT u.id, u.name, o.id AS order_id FROM users u, orders o WHERE u.id o.user_id;这种写法在功能上和INNER JOIN等价但现在不推荐因为把连接条件和过滤条件混在WHERE里可读性差稍不留神漏写WHERE条件就直接变成笛卡尔积。公司规范里一般都会要求显式写INNER JOIN。3.3 左连接LEFT JOIN左表保留全部右表对不上补NULLLEFT JOIN是实际项目里最常用的连接方式它的语义是左表的每一行都保留右表能匹配上的就带上匹配不上的用NULL填充。拿用户和订单举例我想看所有用户的订单情况没买过东西的用户也要列出来不能用INNER JOINSELECT u.id, u.name, o.id AS order_id, o.amount FROM users u LEFT JOIN orders o ON u.id o.user_id;这里没下过单的用户order_id和amount都是NULL。很多报表场景都需要这种“带出全部主表、附加从表数据”的效果。RIGHT JOIN就是把左右反过来右表全保留左表补NULL。我实际项目里几乎不用RIGHT JOIN因为把表顺序换一下改成LEFT JOIN语义更符合人的阅读习惯JOIN写法也更统一。3.4 多张表JOIN连接顺序和执行计划多表JOIN就是在一对一JOIN的基础上继续叠加上去。比如查用户、订单、订单明细三层数据SELECT u.name, o.id AS order_id, oi.product_name, oi.quantity FROM users u INNER JOIN orders o ON u.id o.user_id INNER JOIN order_items oi ON o.id oi.order_id WHERE u.id 1001;这条SQL的重点在于连接顺序有先后先users和orders关联结果集再和order_items关联。MySQL优化器可能会根据统计信息调整实际执行顺序它自己会算哪张表做驱动表更划算但逻辑语义始终是“三张表的交集”。多表JOIN最容易出的问题就是结果集膨胀。订单表和订单明细是1对多关系如果一个订单有3条明细JOIN之后就会变成3行。如果之后再JOIN一张1对多的表行数会成倍增加。统计类需求如果没想清楚这个SUM出来的金额会被人为放大多倍这是新手报表里最经典的翻车现场。后面我会专门演示怎么处理这种重复计算。3.5 ON和WHERE的过滤时机差异LEFT JOIN里ON和WHERE的细微差别能影响结果。看下面这两条SQL-- 情况A过滤条件写在ON里 SELECT u.id, u.name, o.amount FROM users u LEFT JOIN orders o ON u.id o.user_id AND o.status 1; -- 情况B过滤条件写在WHERE里 SELECT u.id, u.name, o.amount FROM users u LEFT JOIN orders o ON u.id o.user_id WHERE o.status 1;情况A是先把orders表里status1的行匹配到左边如果这个用户只有status0的订单匹配不上最后结果是订单相关字段为NULL用户仍然保留。情况B是先做LEFT JOIN再用WHERE过滤掉NULL行本质相当于把LEFT JOIN变成了INNER JOIN没有效订单的用户直接被干掉了。什么时候用哪种写法取决于业务要不要保留没匹配上的左表记录。这俩结果可能完全不一样写之前一定要想清楚。4. 聚合查询和联合查询的组合实战4.1 先JOIN再GROUP BY多表分组统计把JOIN和GROUP BY放一起就能做很多复杂的统计需求。比如“按用户统计每个用户的总消费金额和订单数”需要关联users和orders两张表SELECT u.id, u.name, COUNT(o.id) AS order_count, IFNULL(SUM(o.amount), 0) AS total_amount FROM users u LEFT JOIN orders o ON u.id o.user_id GROUP BY u.id, u.name ORDER BY total_amount DESC;这段SQL的逻辑先连表得到“每个用户对应所有订单”的明细数据集再按用户分组对每组算订单数和总金额。LEFT JOIN保证没下过单的用户也能出现COUNT(o.id)对NULL行数的是0SUM如果为NULL则用IFNULL兜底成0。这里有个实操点GROUP BY u.id和GROUP BY u.id, u.name有区别吗在ONLY_FULL_GROUP_BY模式下只按u.id分组SELECT里的u.name不满足规则会报错。而按u.id, u.name分组就没问题因为id是主键id确定了name就确定了分组结果其实还是一样。更优雅的写法是只按主键分组然后用ANY_VALUE(u.name)绕过报错或者直接GROUP BY u.id, u.name。实际写代码我一般选择后者语义更直白。4.2 聚合后的结果再JOIN先分组再连表有时候先聚合再连表比先连表再聚合更清晰效率也更好。比如查每个分类下销量最高的商品你需要先在商品表里求每个分类的最高销量再关联回商品表拿完整信息。这种场景你如果直接JOIN后GROUP BY反而难以表达“取组内某条完整记录”的语义。用前面的表举例查每个用户最大金额订单的完整信息SELECT o.* FROM orders o INNER JOIN ( SELECT user_id, MAX(amount) AS max_amount FROM orders GROUP BY user_id ) t ON o.user_id t.user_id AND o.amount t.max_amount;这种写法的核心是先做一个“每个用户的最高金额表”这是一个派生表再把它当普通表和订单表关联。注意如果同一个用户有两笔同金额且都是最大金额的订单结果会返回两行。要避免的话可以再用子查询取最小的订单id这个属于面试进阶题。4.3 JOIN导致SUM翻倍的坑与解法前面提到过JOIN碰到1对多关系会让行数膨胀。举个具体案例我要统计每个用户的订单总额和购买商品件数一个订单有多个商品明细-- 错误示例SUM(oi.quantity)可能没算错但订单金额被明细表撑大后重复计算了 SELECT u.id, u.name, SUM(DISTINCT o.id) -- 这不能解决金额问题 FROM users u LEFT JOIN orders o ON u.id o.user_id LEFT JOIN order_items oi ON o.id oi.order_id GROUP BY u.id, u.name;如果直接在已经JOIN明细表的中间结果上SUM(o.amount)每个订单的amount会被重复算多次。正确的做法是拆开算或者用子查询先做一次聚合SELECT u.id, u.name, t1.total_amount, t2.total_items FROM users u LEFT JOIN ( SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id ) t1 ON u.id t1.user_id LEFT JOIN ( SELECT o.user_id, SUM(oi.quantity) AS total_items FROM order_items oi INNER JOIN orders o ON oi.order_id o.id GROUP BY o.user_id ) t2 ON u.id t2.user_id;这就是多表统计最核心的避坑思路不同维度统计别混在一次JOIN里做先分别按维度聚合再在外层关联。4.4 三表联查经典场景统计分析落地再给一个实际项目中经常遇到的综合场景统计每个城市的订单总金额和下单用户数。这里涉及到users城市信息、orders订单金额和用户两张表。一条SQL就能实现SELECT u.city, COUNT(DISTINCT u.id) AS user_count, COUNT(o.id) AS order_count, IFNULL(SUM(o.amount), 0) AS total_amount FROM users u LEFT JOIN orders o ON u.id o.user_id GROUP BY u.city ORDER BY total_amount DESC;这里面的细节COUNT(DISTINCT u.id)是统计有订单的用户数吗不是它统计的是该城市下出现过的用户总数但由于LEFT JOIN如果用户没下过单也照样出现一次。如果只想统计有订单的用户数需要用子查询限定的结果集或者使用CASE WHENSELECT u.city, COUNT(DISTINCT IF(o.id IS NULL, NULL, u.id)) AS active_user_count FROM users u LEFT JOIN orders o ON u.id o.user_id GROUP BY u.city;这里IF表达式把没下过单的用户id变成NULLCOUNT(DISTINCT)会忽略NULL所以统计出来的就是有订单的用户数。也是经典的“不增加子查询就能表内解决”的思路。4.5 索引对连表和分组性能的影响写SQL的时候可以不想索引但SQL慢了就绕不开索引。JOIN和GROUP BY的性能依赖索引尤其明显。JOIN的ON条件列如果能走索引MySQL就能通过索引直接查找匹配行而不是逐行扫描全表。GROUP BY的列也一样有索引就能使用松散索引扫描或者紧凑索引扫描性能天差地别。给前面三张表加上常用索引ALTER TABLE orders ADD INDEX idx_user_id (user_id); ALTER TABLE orders ADD INDEX idx_created_at (created_at); ALTER TABLE order_items ADD INDEX idx_order_id (order_id);实际查询时可以用EXPLAIN确认执行计划EXPLAIN SELECT u.city, COUNT(o.id) AS order_count, IFNULL(SUM(o.amount), 0) AS total_amount FROM users u LEFT JOIN orders o ON u.id o.user_id GROUP BY u.city;重点关注EXPLAIN输出里的type列如果是ALL说明全表扫描需要警惕如果出现index或者ref说明索引被用上了执行计划相对理想。很多慢SQL的问题不是SQL写得不对而是索引没建到位。5. 高频问题排查与面试考点速查5.1 新手最常见的5个执行报错我在各种技术群里回答过不少MySQL问题这里把聚合和连表查询里出现频率最高的几个报错整理一下按照错误类型、触发原因、解决办法做成一张速查表。报错类型常见原因解决办法Expression #N of SELECT list is not in GROUP BY clause选了非聚合列且没放GROUP BY里把列加进GROUP BY或用ANY_VALUE/聚合函数包裹Invalid use of group function在WHERE里用了聚合函数聚合条件挪到HAVINGUnknown column in on clauseON里的列名写错或者表别名对不上检查表别名JOIN条件必须两边都有对应列Column xxx in field list is ambiguous多表JOIN时同名列没有写表前缀所有列都用“表别名.列名”You cant specify target table for update in FROM clauseUPDATE子查询里查询的是同一张表套一层派生表或者改用JOIN UPDATE第六个报错“You cant specify target table ... in FROM clause”在更新场景也常见比如想把超订单用户标记出来-- 报错 UPDATE users SET status 1 WHERE id IN (SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) 5); -- 正确做法套一层派生表 UPDATE users SET status 1 WHERE id IN ( SELECT user_id FROM ( SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) 5 ) t );结合热搜词里有人搜过“mysql中更新子查询”这个坑确实不少人踩。原因很简单MySQL不允许在UPDATE/DELETE语句里直接修改“子查询正在读取的那张表”——这会产生语义上的不确定性套一层派生表让优化器不得不多做一步物化就能绕过去。5.2 聚合函数GROUP BY的排查清单写聚合SQL的时候我会按下面这个清单过一遍基本能避开90%的坑第一步确认SELECT里每列的身份是分组列还是聚合函数还是被ANY_VALUE包住了。如果三者都不是先改SQL。第二步确认过滤条件用在哪里行级过滤用WHERE组级过滤用HAVINGWHERE里有聚合函数就赶紧挪走。第三步确认NULL对结果的影响COUNT(列名)会忽略NULLSUM和AVG也会忽略NULL最后结果是否需要IFNULL兜底。第四步确认连接类型对不对需要保留主表全部行时用LEFT JOIN只需要两边都有匹配时用INNER JOIN。第五步确认多表统计有没有膨胀模型相关联明细表时重点检查SUM的列是否来自“1”这一侧的表。如果是千万别和多行明细JOIN完再SUM。第六步确认GROUP BY后面列的完整度在ONLY_FULL_GROUP_BY模式开启时SELECT中所有非聚合列都要写进GROUP BY。这份清单也推荐直接当成团队code review的标准检查项能省掉很多低级bug不然每次上线前都在那排查“为什么金额多了一倍”的问题。5.3 面试常考的几条SQL手写题MySQL面试必考的一部分就是聚合查询和JOIN查询我来列几个典型题目顺便把正确答案也写出来方便大家自查。题目一查询每个部门工资最高的员工经典分组取最大假设有一张employee表字段是dept_id、name、salary。一条思路是先按部门分组求最高工资再关联回员工表SELECT e.* FROM employee e INNER JOIN ( SELECT dept_id, MAX(salary) AS max_salary FROM employee GROUP BY dept_id ) t ON e.dept_id t.dept_id AND e.salary t.max_salary;题目二查询没有订单的用户用LEFT JOIN配合IS NULL是最经典的写法SELECT u.* FROM users u LEFT JOIN orders o ON u.id o.user_id WHERE o.id IS NULL;这里用o.id IS NULL判断右表有没有匹配上比用NOT IN子查询更高效尤其是右表数据量大的时候。NOT IN里面如果子查询结果包含NULL会直接导致整条SQL查询结果为空这是很隐蔽的坑。题目三统计每个用户的订单数以及他们的消费总额订单数超过3的显示出来SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders GROUP BY user_id HAVING COUNT(*) 3;5.4 连表查询性能优化思路最后一个部分聊下怎么把SQL写快。聚合和JOIN本身不是原罪慢SQL大多是索引问题、数据模型问题和写法问题。第一JOIN的关联列必须建索引。你在orders.user_id上建了索引JOIN的性能就会明显提升因为MySQL可以用索引去右表快速定位匹配的行。没索引的话每处理左表一行都要全表扫一次右表这个复杂度是O(N×M)数据量上万就会卡。第二避免在JOIN条件上做函数运算。比如ON DATE(o.created_at) DATE(u.created_at)SQL里任何对列做函数包裹的操作都会让索引失效哪怕这列本身有索引也没用。正确做法是提前算好边界值WHERE o.created_at 2024-01-01 AND o.created_at 2024-02-01第三分组统计时先过滤再分组。WHERE应该尽量把数据范围缩小减少GROUP BY处理的数据量。比如统计近30天的数据先写WHERE created_at条件再GROUP BY。第四只查需要的列。SELECT * 会让MySQL把所有字段都捞出来遇到TEXT、BLOB类型的大字段还会拖慢IO。日常开发和线上问题排查时尽量明确写出需要的列。第五分析慢SQL时用EXPLAIN。EXPLAIN不执行SQL只是输出执行计划判断有没有走索引、扫描了多少行、临时表怎么用的。看到Using filesort和Using temporary特别多的时候就要警惕排序和分组带来的磁盘开销。我在定位慢SQL时一般会先去看EXPLAIN里的type从好到差大致是const → eq_ref → ref → range → index → ALL。如果看到最右侧的ALL先建索引再说其它。最后说一点实操体会我在实际项目里有一个强烈的体会SQL写得清不清楚直接影响后面所有人维护的体验。聚合和JOIN这种基础能力看起来谁都会但真正写出条理清晰、结果准确、性能可靠的SQL靠的是对数据语义和SQL执行顺序的深刻理解。GROUP BY什么时候能用、LEFT JOIN什么时候会丢数据、什么时候SUM会翻倍这些细节我今天全部摊开讲了如果看完能让你少踩一个坑这篇就值了。还有一个实操中非常实用的小技巧凡是多表聚合类的复杂SQL先不要急着一次写完拆成几步每个子查询单独验证结果。比如先单独跑子查询看聚合对不对再在外层做JOIN最后套外层过滤。拆开来排查定位问题的速度比盯着一条几百行的SQL发呆快得多。先确认每个子查询的数据量是否符合预期再叠加上一层这样出问题时你能立刻知道是哪一步引入了偏差不光是聚合和JOIN几乎所有的SQL疑难杂症都能用这个笨招解决。