MySQL DQL查询实战:SELECT执行顺序、分组聚合与多表连接全解析

数据查询语言(Data Query Language,DQL)是MySQL课程里最值得花时间啃的一块,也是我从带新人过程中感触最深的部分。DQL语句的核心就是SELECT,工作中无论是业务取数、报表统计、数据核对还是排查线上问题,95%的场景都是在和SELECT打交道。很多人把DQL理解成"背几条查询语法",其实完全不是一回事:真正决定查询结果对不对、跑得快不快的,是你对执行顺序、过滤时机、连接方式和分组统计口径的理解。这篇内容是MySQL基础系列的第二讲,我把课堂上的讲解顺序和学员经常踩的坑放在一起,适合刚学完建表、插入、修改和删除,正准备进入查询阶段的人。耐心看完,配合练习,比零散翻文档要高效得多。

1. 为什么DQL是MySQL课程里的"分水岭"

1.1 DQL在SQL语言族中的位置

初学MySQL时最先接触到的是四大类SQL:数据定义语言DDL、数据操作语言DML、数据查询语言DQL、数据控制语言DCL。其中DDL管建库建表,DML管插入、修改、删除,DQL管查询,DCL管权限。表面上看,DQL只是"读数据",但实际工作中它是被调用最频繁的一类语句。

一个很直观的例子:业务系统每天产生的订单、用户行为、设备状态数据,最终都要通过查询变成报表和分析结论。写接口时需要查询,做数据分析时需要查询,定位脏数据时需要查询。DML操作前通常还要先查询确认影响范围,避免误删。所以很多面试题、实训环节都会把DQL作为重点考察对象,因为它最能反映你对数据结构的理解程度。

还有一个容易被忽略的点:DQL并不只是简单的"把数据列出来"。它背后包含过滤、分组、聚合、排序、分页、连接、子查询这些能力。这些东西组合起来,几乎可以完成你面对的所有取数需求。学透了DQL,再去看视图、临时表、存储过程,会发现那些都只是查询能力的延伸。

1.2 学DQL到底在学什么

我带实训班时候,会把DQL学习拆成三个层次:

第一个层次是语法层,也就是写得出能跑的SQL。比如知道SELECT后面可以跟列名、函数、表达式,知道WHERE后面写条件,知道GROUP BY怎么分组。这个层次只要多写就能过。

第二个层次是逻辑层,也就是知道SQL怎么执行。很多人卡在这一层,症状是"结果和预期不一致"。比如为什么WHERE里不能直接用聚合函数,为什么HAVING能做而WHERE不能做,为什么ORDER BY写在最后却会影响分页结果。这些都需要理解执行顺序。

第三个层次是性能层。同样的查询,写法不同执行效率可能差几十倍。比如大表深分页为什么越翻越慢,连接查询为什么建了索引才快,子查询为什么会被改写。这些内容基础课只会浅尝辄止,但作为新人,你至少应该知道什么写法是危险的。

这篇博文会按照"语法——逻辑——性能"的思路往下走,尽量把每个关键点都讲透。

2. SELECT骨架与执行顺序的底层逻辑

2.1 SELECT语法骨架先看哪几块

任何一条查询语句,都可以套进这个骨架里:

SELECT [DISTINCT] 列名/表达式/聚合函数 FROM 表名 [WHERE 行级过滤条件] [GROUP BY 分组字段] [HAVING 分组后的过滤条件] [ORDER BY 排序字段] [LIMIT 偏移量, 条数]

方括号里的都是可选项,唯一必须写的是SELECT和FROM。初学者最喜欢犯的错误是上来就写一堆条件,结果字段、表、条件之间的关系是乱的。我的建议是永远从最简形式开始:先写SELECT * FROM 表名确认数据存在,再一步步叠加过滤、分组、排序,每加一层就运行一次观察结果变化。

这里要说一个写SQL的基本态度:能简单就不要复杂。一条能跑且语义清楚的查询,远比一条炫技但没人看得懂的查询有价值。

2.2 书写顺序不等于执行顺序

这一点如果不理解,后面学WHERE、GROUP BY、HAVING都会踩坑。SQL语句的书写顺序和数据库引擎的实际执行顺序是不一样的。

实际的逻辑执行顺序大致是:

顺序步骤说明
1FROM确定数据来源,加载表
2WHERE对表中的每一行进行过滤
3GROUP BY按字段分组
4HAVING对分组后的结果过滤
5SELECT计算要输出的列和表达式
6DISTINCT对结果去重
7ORDER BY对结果排序
8LIMIT截取指定行数

为什么这个顺序如此重要?因为它直接解释了很多"怪现象"。

举个例子:WHERE发生在GROUP BY之前,所以WHERE里不能引用聚合函数的结果。你不可能在分组前就判断COUNT(*) > 5,因为这个时候还没有分组完成。同样的道理,SELECT里定义的列别名,在WHERE阶段还没有生成,所以WHERE里不能用别名过滤,但ORDER BY阶段可以,因为执行到ORDER BY时SELECT已经算完了。

我用一个生活化的类比来解释:执行顺序就像做饭的过程。先买菜(FROM),再按新鲜程度挑拣食材(WHERE),然后把挑好的菜按种类分装(GROUP BY),分装后再决定哪些种类的菜上桌(HAVING),接着切菜摆盘(SELECT),最后按颜色顺序排列上桌(ORDER BY),只端出前几盘(LIMIT)。如果你在"挑拣食材"阶段就想知道"某类菜一共有几盘",那是做不到的,因为还没到分装那一步。

记住这张表,后面所有疑难问题都能往这个框架里套。

2.3 基础查询实操:去重、别名、表达式

先看一段最基础也是最常用的写法:

SELECT stu_id, name, age + 1 AS age_next_year FROM student;

这里age + 1是对原始值的表达式运算,AS age_next_year是给计算列起别名。别名可以省略AS直接写空格,例如age + 1 age_next_year,但我建议显式写AS,语义更清楚,团队协作也不会产生歧义。

去重用DISTINCT,例如查看一共有哪些班级编号:

SELECT DISTINCT class_id FROM student;

注意DISTINCT作用于后面所有列的组合,而不是单单第一列。比如SELECT DISTINCT class_id, gender的意思是"班级和性别的组合不重复",不是"只看班级不重复"。新手经常在这里踩坑,把组合去重误当成单列去重。

另外,查询常量列也是一种常见用法。比如在报表里加一列固定的标签:

SELECT '2024年统计' AS year_label, class_id FROM student;

常量列不依赖表字段,经常用来给后续程序处理做标记。基础但不冷门。

3. WHERE过滤与运算符:常见陷阱逐一套牢

3.1 WHERE与HAVING的分工边界

WHERE是行级过滤,发生在分组之前;HAVING是分组级过滤,发生在分组之后。这个边界必须刻在脑子里。

下面这个查询是按班级统计人数,但只想看人数大于30人的班级。正确的写法是:

SELECT class_id, COUNT(*) AS cnt FROM student GROUP BY class_id HAVING cnt > 30;

如果把HAVING换成WHERE:

SELECT class_id, COUNT(*) AS cnt FROM student WHERE COUNT(*) > 30 GROUP BY class_id;

这条语句会直接报错,因为WHERE执行时GROUP BY还没有执行,COUNT(*)在WHERE阶段是不允许使用的。记住:WHERE处理的是"行"的条件,HAVING处理的是"组"的条件。一个常见的扩展误区是:有些人为了让查询快一点,恨不得把所有条件都塞进WHERE。这不完全对。WHERE能做的事确实应该在WHERE做,因为它能提前减少数据量;但如果条件是针对聚合结果的,就必须放在HAVING,放WHERE里要么报错要么语义错误。

为了效率,可以在WHERE里先过滤部分行,再做分组统计:

SELECT class_id, COUNT(*) AS cnt FROM student WHERE status = 1 GROUP BY class_id HAVING cnt > 30;

这里status = 1先排除掉无效学生,再分组统计,性能自然更好。写复杂查询之前,先画一下"哪些条件属于哪一步"很有帮助。

3.2 运算符使用中的经典坑

WHERE后面几乎天天在跟运算符打交道,但有四个坑让我印象特别深。

第一个坑是NULL的判断。很多新人写出这样的语句:

SELECT * FROM student WHERE phone = NULL;

这条语句查不出任何结果,因为= NULL不是一个合法条件。NULL表示"未知值",判断未知值只能用IS NULL或IS NOT NULL。正确的写法是:

SELECT * FROM student WHERE phone IS NULL;

为什么会这样?因为NULL不是值,它无法参与等值比较。把NULL理解成"这里的数据暂时缺失"就很好想通了,你不知道缺失内容和某个具体值是否相等,所以= NULL永远返回未知。

第二个坑是空字符串和NULL的区别。空字符串是一个长度为0的值,NULL是缺失。统计电话为空的人时,如果既包含NULL又包含空字符串,需要写:

SELECT * FROM student WHERE phone IS NULL OR phone = '';

不能只写其中一个,否则数据会漏。

第三个坑是字符串和数字的隐式转换。假设手机号以varchar类型存储,你写了WHERE phone = 13812345678,MySQL会对字符串列做隐式转换,如果手机号里有少量非数字字符,可能匹配不到,甚至会导致索引失效。稳妥的做法是等号右边也加引号:

WHERE phone = '13812345678';

第四个坑是日期字符串和日期边界。字段类型是datetime时,判断某一天的数据,如果写:

WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31'

那实际会查到2024-01-31 00:00:00及之前的数据,2024-01-31当天零点之后的数据全部丢失。原因在于BETWEEN是闭区间,会把上下界当成一个完整的比较。更稳妥的写法是:

WHERE order_date >= '2024-01-01' AND order_date < '2024-02-01';

前半条包含1月1日零点,后半条排除2月1日零点,正好覆盖整个1月。这条经验我在统计订单、日志场景里反复用到,可以说是日期查询里最重要的习惯之一。

3.3 实测中的排查:条件写对但数据对不上

有一次实训课上,有个同学统计部门人数,得到的人数比预期少很多。他的SQL简化版是这样的:

SELECT department_id, COUNT(*) FROM employee WHERE salary > 5000 GROUP BY department_id;

从语法结构上挑不出毛病,但数据就是不对。后来检查发现,employee表里面工资字段设计成了varchar,里面混入了几个带单位的字符串,导致和数字常量5000比较时,那些行被过滤掉了。这类问题用肉眼很难发现,排查方式是把过滤条件逐步放宽:

SELECT COUNT(*) FROM employee; -- 总数 SELECT COUNT(*) FROM employee WHERE salary IS NOT NULL; -- 非空 SELECT COUNT(*) FROM employee WHERE salary > 5000; -- 过滤后

三段对比,很快就能定位是条件本身、数据类型还是空值的问题。这也是我想强调的调试习惯:不要只盯着一句SQL看,要分段验证行数变化。

4. 聚合函数与GROUP BY分组统计实战

4.1 COUNT、SUM、AVG、MAX、MIN的计算口径

聚合函数把多行数据合成一个结果,最常见的就是这些:

SELECT COUNT(*) AS total_cnt, COUNT(score) AS score_cnt, SUM(score) AS total_score, AVG(score) AS avg_score, MAX(score) AS max_score, MIN(score) AS min_score FROM exam_result;

这五个函数里最容易出错的是COUNT。COUNT(*)会统计所有行数,包括某个字段为NULL的行;而COUNT(score)只统计score字段非NULL的行数。如果字段里NULL值很多,两个数字会差得很明显,这在做数据质量检查时是一个重要线索。

SUM和AVG同样会忽略NULL值。比如某门课三个人参加,成绩分别是80、90和NULL,AVG(score)计算结果是85而不是56.7,因为NULL那一行被直接忽略了。如果你希望把缺考的人按0分计入平均分,就需要写:

AVG(IFNULL(score, 0))

IFNULL把NULL替换成0,语义完全不同。另一个细节是SUM对一个空结果集返回NULL而不是0,所以判断"销售总额是否是0"和"是否没有销售记录"要用不同SQL。

4.2 GROUP BY的分组逻辑再拆一下

GROUP BY的作用可以理解为:把多行数据按照一个或几个字段的取值归并成若干组,组内行数可能很多,但最终每个组只产出一行汇总结果。

一个典型的误区是:既然分组成一个组,为什么SELECT里面可以混着写非分组字段?比如:

SELECT class_id, gender, COUNT(*) FROM student GROUP BY class_id;

这条语句在MySQL的默认模式下不会报错,但gender的返回结果是随机的,因为它不属于分组字段。这在业务上是一个隐患。标准SQL的思路是:SELECT后面只能出现分组字段和聚合函数,其他字段都不应该出现。MySQL 8.0默认开启了ONLY_FULL_GROUP_BY模式后,这种写法会直接报错,这是很好的保护。

按多个字段分组也很常见,例如按班级再按性别统计:

SELECT class_id, gender, COUNT(*) AS cnt FROM student GROUP BY class_id, gender;

执行逻辑是先按class_id分一层,再在class_id内部按gender分一层,最终每个班级下男生女生各一行。多列分组的顺序会影响结果排序但不影响分组的整体结构。

4.3 分组后过滤:HAVING的正确打开方式

HAVING在GROUP BY之后执行,所以它可以使用聚合函数和别名。以统计各科平均分大于75分为例:

SELECT subject, AVG(score) AS avg_score FROM exam_result GROUP BY subject HAVING AVG(score) > 75;

这里的AVG(score) > 75不能移入WHERE,因为平均值是分组之后才能算出来的。HAVING还可以搭配多个条件:

HAVING AVG(score) > 75 AND COUNT(*) >= 10

表示"平均分超过75且参加人数不少于10人"的科目。这个场景在筛选有效统计样本时非常实用。

提醒一个细节:MySQL允许在HAVING里直接使用SELECT列别名,例如HAVING avg_score > 75,但SQL标准并不推荐,因为逻辑顺序上HAVING先于SELECT,依赖别名会让人混淆执行顺序。我自己的习惯是写完整表达式或者聚合函数,少用别名。

5. ORDER BY排序与LIMIT分页的配合细节

5.1 排序规则与多列排序

ORDER BY负责结果排序,默认是升序ASC,降序用DESC。一个很容易忽略的点是:排序是在SELECT之后执行的,所以ORDER BY可以使用SELECT中的别名,比如:

SELECT subject, AVG(score) AS avg_score FROM exam_result GROUP BY subject ORDER BY avg_score DESC;

多列排序的写法是:

SELECT emp_id, department_id, salary FROM employee ORDER BY department_id ASC, salary DESC;

执行逻辑很直接:先按第一列排序,第一列值相同的记录再按第二列排序。这个顺序写反了效果完全不同,很多人以为多列排序是"同时按两个字段排序",其实不是,它是嵌套的排序规则。

NULL的排序位置也需要注意。MySQL默认升序时NULL排在最前面,降序时NULL排在最后面。如果你希望把NULL排到最后,可以写成:

ORDER BY ISNULL(phone), phone

这里的ISNULL(phone)对NULL返回1,对非NULL返回0,升序时0排在1前面,实际效果就是NULL记录被放到后面。

5.2 LIMIT分页公式与深分页问题

分页是后端开发里绕不开的需求,语法是:

SELECT * FROM employee ORDER BY emp_id LIMIT 20, 10;

这里表示跳过前20条,返回从第21条开始的10条。如果参数是页码page和每页条数pageSize,通用公式是:

LIMIT (page - 1) * pageSize, pageSize

第一页就是LIMIT 0, 10,第二页LIMIT 10, 10。这里最容易犯的错误是把两个参数顺序写反。LIMIT的第一个参数是偏移量,第二个是条数,很多人想当然地先写条数后写偏移量,结果返回了错误的数据。

关于分页有一个性能问题必须知道:深分页越翻越慢。比如LIMIT 100000, 10要扫描并丢弃前100000条,性能就会很差。常用优化方式是记住上一页的最后一条主键,然后取主键大于它的固定条数:

SELECT * FROM employee WHERE emp_id > 100234 ORDER BY emp_id LIMIT 10;

这种"基于游标"的分页方式不依赖偏移量,在数据量大的场景下效果很明显。如果是带复杂过滤条件的查询,先把过滤结果缩小范围再分页,也会有帮助。

6. 多表连接:INNER JOIN之外的选择

6.1 连接的本质是笛卡尔积加条件过滤

多表连接是DQL里让新手最头疼的部分,但其实一句话就能说破:连接就是把两张表的数据做笛卡尔积,然后通过ON条件过滤掉不匹配的行。

假设学生表有10行,成绩表有20行,没有任何条件的连接:

SELECT * FROM student JOIN score;

会得到10乘20等于200行,这种结果通常没有业务意义。如果加上连接条件:

SELECT * FROM student JOIN score ON student.stu_id = score.stu_id;

就只保留stu_id相等的记录。从这个角度看,ON条件就像一个作用于连接后结果的WHERE条件,只不过它专门负责定义两张表之间的关联关系。

容易犯的错误是漏掉ON条件或写错关联字段。执行计划一旦出现200行这种爆炸式增长,第一反应就应该是检查连接条件。

6.2 三种JOIN的使用场景对照

常用的连接类型有三种,适合的场景差异很大:

连接类型语义典型业务场景
INNER JOIN只返回两张表都能匹配上的行查有成绩记录的学生
LEFT JOIN返回左表全部行,右表有匹配就填充,没有匹配显示NULL查所有学生及其成绩,没考试的学生也要列出来
RIGHT JOIN返回右表全部行,左表有匹配就填充,没有匹配显示NULL上面场景反过来,但主流写法都习惯用LEFT JOIN

一个实际的例子,查所有学生和他们的成绩,如果某个学生没有成绩记录,也要出现在结果里:

SELECT student.name, score.score FROM student LEFT JOIN score ON student.stu_id = score.stu_id;

这条查询返回的行数至少等于学生表行数。如果学生表有5人而成绩表只有3条,结果会有5行,其中两行score列是NULL。

有一种容易看走眼的情况:在LEFT JOIN里把右表的字段放进WHERE,比如:

SELECT student.name, score.score FROM student LEFT JOIN score ON student.stu_id = score.stu_id WHERE score.score > 60;

这会把那两个还没有成绩的学生从结果里过滤掉,因为NULL无法满足score > 60。实际效果就变成了INNER JOIN。如果需求是"只看成绩大于60的学生",这么写没问题;但如果需求是"所有学生都要显示,即使成绩缺失",那么这个条件是错误的写法,要放到ON里面去:

LEFT JOIN score ON student.stu_id = score.stu_id AND score.score > 60;

这个细节几乎每次讲连接查询都会有人犯,因为它混淆了连接条件和过滤条件的作用时机。

6.3 连接查询的性能注意事项

连接查询的性能主要看三点。

第一,连接字段要有索引。没有索引时,数据库每取左表一行,都要在右表里做一次全表扫描,嵌套循环下来成本极高。这也是为什么外键字段、频繁关联的字段通常建议建索引。

第二,小表驱动大表。嵌套循环连接中,外表越小,循环次数越少。优化器通常会自己选择驱动顺序,但当你使用STRAIGHT_JOIN或面对极端数据分布时,可以人工干预。

第三,先过滤再连接。尽量让WHERE条件把单个表的数据量缩小后再参与连接。逻辑上连接是先生成笛卡尔积,但优化器会优先执行条件下推,把单表过滤提前。

我在实际排查慢查询时,习惯先用EXPLAIN看执行计划里的扫描行数。如果发现某个连接字段的type是ALL(全表扫描),第一件事就是检查索引。这些虽然已经超出基础语法的范畴,但从开始学连接查询就带上性能意识,后面会省很多事。

7. 子查询的四种形态与性能辨析

7.1 WHERE子查询、FROM子查询与标量子查询

子查询就是"查询里面套查询"。按出现位置可以分为几种形态,我会按实用频率排列。

第一种是WHERE子查询,最常见。例如查出考试成绩高于全班平均分的学生:

SELECT stu_id, score FROM exam_result WHERE score > ( SELECT AVG(score) FROM exam_result );

括号里的子查询先算出平均分,外层查询再用这个值逐行比较。这个子查询只执行一次,返回单个值,所以也叫标量子查询。

第二种是FROM子查询,也叫派生表。例如先按班级算出平均分,再过滤平均分大于80的班级:

SELECT class_id, avg_score FROM ( SELECT class_id, AVG(score) AS avg_score FROM exam_result GROUP BY class_id ) AS t WHERE avg_score > 80;

FROM后面括号里的结果相当于一张临时表,必须给它起一个别名,也就是AS t。忘记写别名会报错,这是新手常见的踩坑点。

第三种是SELECT子查询,直接出现在SELECT列中,通常用来配合外层查询做逐行计算。不过这类查询在数据量大时性能容易劣化,能改成JOIN就尽量改成JOIN。

第四种是EXISTS子查询,用来判断"是否存在"而不是返回具体值。例如查出有成绩记录的学生:

SELECT name FROM student WHERE EXISTS ( SELECT 1 FROM exam_result WHERE exam_result.stu_id = student.stu_id );

子查询里写SELECT 1而不是SELECT *是一种习惯,因为EXISTS只关心有没有匹配,不关心返回什么列,写1可以减少无谓的开销。

7.2 IN与EXISTS的取舍

IN和EXISTS都能表达"在某个集合里",但适用场景有差异。简单的集合判断,比如查班级编号在某个列表里的学生:

SELECT * FROM student WHERE class_id IN (1, 2, 3);

这种写法简单直观,数据量小的时候就该这么写。

但如果外层表很大、内层子查询数据量也不小,情况就复杂了。经典经验是:外层表数据量大、内层表数据量小时,IN往往表现更好;反之,对于带关联条件的EXISTS,子查询通常可以利用索引提前停止扫描。不过MySQL 8.0的优化器已经足够聪明,很多IN子查询会被自动改写成半连接,性能不一定差。我的建议是:先写语义清晰的版本,遇到性能问题再结合EXPLAIN去判断是否需要改写,不要凭模糊的印象做出不合理的优化。

FROM子查询和JOIN经常可以互换。上面按班级平均分的例子,也可以通过连接加分组实现。选哪种的核心是看可读性和执行计划。对新人来说,我倾向于先掌握JOIN,因为它更容易看清关联关系,子查询则适合表达"先算出一个集合再作为条件"的逻辑。

8. 实操中出现的高频报错和排查习惯

8.1 三条高频报错的定位方式

实际写SQL时,报错信息是最好的老师。我总结三个高频率报错和对应的原因。

第一个报错是Every derived table must have its own alias。这是FROM子查询没有写别名导致的。MySQL要求派生表必须有一个名字,所以哪怕临时表不引用,也必须写AS t之类的别名。

第二个报错是Invalid use of group function。这个报错的本质是用了聚合函数却不满足使用条件,常见原因就是把COUNT、SUM写在WHERE里。看到这个报错,先想清楚:这个聚合是在分组前还是分组后?如果是分组后的条件,把它放进HAVING。

第三个报错是Unknown column 'xxx' in 'where clause'。这个报错通常是列名拼写错误、表名没写对,或者表达式别名被用在了WHERE里。遇到这种报错,可以先查看表结构确认字段名,再把WHERE阶段不能用的别名移动到ORDER BY或HAVING里。

8.2 分步验证和EXPLAIN的实用习惯

写复杂查询时,我最推荐的调试方法是从简到繁逐层叠加。比如一条需求是"统计每个班级平均分大于80分的男生人数",我会分成几步来写:

-- 第一步:先确认基础表数据 SELECT * FROM student WHERE gender = '男'; -- 第二步:加上成绩关联 SELECT student.class_id, exam_result.score FROM student JOIN exam_result ON student.stu_id = exam_result.stu_id WHERE student.gender = '男'; -- 第三步:分组聚合 SELECT student.class_id, COUNT(*) AS cnt, AVG(exam_result.score) AS avg_score FROM student JOIN exam_result ON student.stu_id = exam_result.stu_id WHERE student.gender = '男' GROUP BY student.class_id; -- 第四步:最后过滤分组结果 SELECT class_id, cnt, avg_score FROM ( SELECT student.class_id, COUNT(*) AS cnt, AVG(exam_result.score) AS avg_score FROM student JOIN exam_result ON student.stu_id = exam_result.stu_id WHERE student.gender = '男' GROUP BY student.class_id ) AS t WHERE avg_score > 80;

每一步都先跑通,结果不对就知道是哪一步的过滤条件或聚合口径出了问题。这种习惯比一次性写好再猜问题要快得多。

EXPLAIN也是排查性能问题的核心工具,用法很简单,只需要在查询语句前面加EXPLAIN:

EXPLAIN SELECT ...;

看结果不需要全部理解,只要抓住几个关键字段:type、key和rows。type如果出现ALL,说明在做全表扫描,数据量大时需要检查索引;key显示实际用到的索引;rows是预估扫描行数。数值异常大时,优先怀疑连接缺少索引、WHERE条件无法用索引,或子查询被反复执行。

8.3 一条查询串起所有知识点

最后用一个完整示例把整篇内容串起来。假设要统计"每个班级中数学成绩合格的人数,以及这些人数学成绩的平均分,只看人数超过5人的班级,按平均分从高到低排序,取前3个班级":

SELECT class_id, COUNT(*) AS pass_cnt, AVG(score) AS avg_score FROM exam_result WHERE subject = 'math' AND score >= 60 GROUP BY class_id HAVING pass_cnt > 5 ORDER BY avg_score DESC LIMIT 3;

这条语句涵盖了WHERE行级过滤、GROUP BY分组、HAVING分组后过滤、ORDER BY排序、LIMIT分页,以及COUNT、AVG两个聚合函数。你把它和最初那张执行顺序表对照着看,就能感受到一条SQL在数据库里到底经历了哪些步骤。

我在实际操作中最深的体会是:不要急着把所有知识点一次性塞进脑子里。先把单表查询和过滤练熟,再叠加分组统计,之后再来啃连接和子查询。每个阶段都用上面的分步验证方法写SQL,遇到报错就回到执行顺序表里找原因。基础如果打不牢,后面写日报、查线上数据、做业务分析的时候都会觉得很吃力。希望这篇内容能帮你把DQL这块"硬骨头"啃下来,少走一些我当年走过的弯路。

相关新闻

品牌短视频爆款方法论:从算法推荐到Z世代内容策略

品牌短视频爆款方法论:从算法推荐到Z世代内容策略

先说个有点反常识的结论&#xff1a;那些在短视频平台上一夜刷屏的品牌官方视频&#xff0c;真正厉害的往往不是制作预算&#xff0c;也不是明星阵容&#xff0c;而是它看起来几乎不像官方做的东西。最近某科技公司&#xff08;下文我就用果厂代称&#xff09;一条 TikTok 视频…

2026/10/20 16:48:58 阅读更多 →
贝叶斯网络变量独立性分析:d-分离与马尔可夫毯实战指南

贝叶斯网络变量独立性分析:d-分离与马尔可夫毯实战指南

1. 从“变量之间到底有没有关系”说起很多人第一次接触贝叶斯网络&#xff0c;注意力都放在条件概率表怎么填、推理算法怎么跑&#xff0c;却忽略了一个更根本的问题&#xff1a;这张图本身就已经把变量之间的独立性关系说清楚了。换句话说&#xff0c;你不需要真的去算一遍联合…

2026/10/20 16:48:58 阅读更多 →
数据集成平台选型指南:从连接器生态到五年成本模型的七维评估

数据集成平台选型指南:从连接器生态到五年成本模型的七维评估

企业数据集成平台选型这件事&#xff0c;我见过太多团队栽跟头。不是产品不够好&#xff0c;而是选型的方法论出了问题&#xff1a;要么被厂商演示的炫酷界面带偏&#xff0c;要么只盯着采购价格忽略了后面三年的运维成本&#xff0c;更有甚者&#xff0c;连自己的数据规模和数…

2026/10/20 16:48:58 阅读更多 →

最新新闻

这6个底层技能,比学一堆工具更不容易过时

这6个底层技能,比学一堆工具更不容易过时

这6个“常用技能”&#xff0c;我一开始全理解错了。当时看项目标题“分享6个最常用的skill”&#xff0c;我第一反应是准备一份技术栈清单&#xff0c;甚至把某段时间特别火的几类工具关键词都列了出来。但和朋友聊完、又翻了几次项目正文之后才发现&#xff0c;真正的核心不是…

2026/10/20 17:34:07 阅读更多 →
工业控制系统安全工程师(CISP-ICSSE)全解析:报考条件、知识体系与职业价值

工业控制系统安全工程师(CISP-ICSSE)全解析:报考条件、知识体系与职业价值

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 17:34:07 阅读更多 →
Ruby重构机票电商平台:事务、缓存与高并发实战解析

Ruby重构机票电商平台:事务、缓存与高并发实战解析

简介&#xff1a;eyoo旅游机票电商平台重构版是一份基于Ruby技术栈的在线机票销售系统学习资源&#xff0c;适合希望掌握Ruby on Rails电商项目完整开发流程的开发者&#xff0c;用于研究业务建模、数据库关联、接口设计与部署方案。压缩包大小约2.62MB&#xff0c;当前标注文件…

2026/10/20 17:34:07 阅读更多 →
AI论文写作软件怎么用?专科生毕业论文从选题到答辩的实用指南

AI论文写作软件怎么用?专科生毕业论文从选题到答辩的实用指南

如果你现在手机相册里至少存了五张“学霸同款2026 TOP10 AI论文写作软件推荐图”&#xff0c;但论文还卡在“题目改了四遍都没定下来”&#xff0c;那这篇内容就是为你准备的。我不打算继续报一遍软件名字&#xff0c;因为真正决定你论文能不能顺利过关的&#xff0c;从来不是榜…

2026/10/20 17:34:07 阅读更多 →
2024最新Node.js下载安装与环境变量配置完整指南

2024最新Node.js下载安装与环境变量配置完整指南

如果你在前端、后端或者全栈开发这条路上待过一段时间&#xff0c;Node.js这个名字一定绕不开。它本质上是一个基于V8引擎的JavaScript运行时&#xff0c;把原本只能在浏览器里跑的JavaScript带到了操作系统层面。装上它之后&#xff0c;你可以用JavaScript写命令行工具、写后端…

2026/10/20 17:34:07 阅读更多 →
Ubuntu下PostgreSQL安装配置与Navicat连接指南

Ubuntu下PostgreSQL安装配置与Navicat连接指南

做后端开发或者数据相关工作的朋友&#xff0c;应该都经历过这么一幕&#xff1a;好不容易在 Ubuntu 上把 PostgreSQL 装好了&#xff0c;却发现命令行操作没问题&#xff0c;客户端工具却怎么都连不上&#xff1b;或者反过来&#xff0c;Navicat 装好了&#xff0c;连接时报一…

2026/10/20 17:33:06 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →