ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL DQL单表查询从入门到实践:SELECT、WHERE、GROUP BY与性能优化

MySQL DQL单表查询从入门到实践:SELECT、WHERE、GROUP BY与性能优化 MySQL 的 DQL 可能是你接触 SQL 时第一个真正意义上的“主力语句”。无论是后端开发、数据分析、运维排查还是单纯想搞定面试里的手写 SQL 题单表查询都是绕不过去的底座。很多人学 SQL 卡壳不是因为语法记不住而是没搞明白执行顺序和逻辑顺序的区别一遇到稍微复杂的查询就不知道从哪下手。这篇文章我会直接用一套极其朴素的思路把 DQL 单表查询从头拆到尾——从最基础的 SELECT 语法讲起到 WHERE 过滤、聚合分组、排序分页、去重与条件分支再到那些你日后实际工作中一定会踩的坑。全程配合一张简单的成绩表做示例每一个语句都是可以直接复制到你的 MySQL 里跑起来的零基础也能跟着一步步走完。1. 整体设计与思路拆解1.1 DQL 在 SQL 中的定位与价值结构化查询语言SQL按功能可以拆成好几块DDL 管建表改表DML 管增删改数据DCL 管权限。而 DQL也就是 Data Query Language专职负责一件事查数据。它是你与数据库对话最频繁的入口一套系统里 90% 以上的数据库操作都是查询。单表查询作为 DQL 的基础解决的痛点是“我有一张表里面的数据又杂又多我该怎么按条件把它捞出来、算清楚、排好序”。所有未来你学到的多表联查、子查询、视图、窗口函数本质上都是在单表查询的结果集上做二次加工。如果单表查询的功底不扎实后面越学越痛苦——你分不清哪个条件应该放在 WHERE 后面哪个条件必须交给 HAVING也搞不懂为什么某些查询慢得离谱。我给你打个比方。单表查询就像是一间只有一个货架的仓库你要做的事是从这个货架上挑出你需要的商品按某个规则摆放整齐再统计一下总价。听起来很简单但仓库管理员之间的差距恰恰就体现在“挑得快、挑得准、摆得明白”这几件事上。DQL 单表查询修炼的就是这个基本功。1.2 学习路径规划从简单到复杂这篇文章的编排思路严格遵循“先跑通再理解后优化”的路径。很多人一上来就刷那种大而全的 SQL 题库结果一看答案觉得都懂一动手全懵。原因就是没有在基础阶段构建起自己的查询思路。我的建议是三步走。第一步掌握 SELECT 框架和 WHERE 过滤这是所有查询的地基第二步掌握聚合函数与 GROUP BY 分组这是数据统计的核心武器第三步掌握排序、分页与条件分支这是让查询结果真正“可用”的最后一公里。每一步我都在下面配了实战场景和可直接运行的示例你跟着敲完基本就能覆盖日常工作中 80% 的单表查询需求。2. 环境准备与基础语法框架2.1 准备一张能折腾的测试表学习 SQL 最忌讳的就是只看不练。工欲善其事必先利其器我们先在 MySQL 里建一张简单的学生成绩表后续所有示例都在这张表上操作。建表是 DDL 的活不属于本文重点但为了让查询能跑起来这一步必须做。建表语句如下CREATE TABLE student_score ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, student_name VARCHAR(50) NOT NULL COMMENT 学生姓名, major VARCHAR(50) COMMENT 专业, subject VARCHAR(50) COMMENT 科目, score DECIMAL(5,1) COMMENT 成绩, exam_date DATE COMMENT 考试日期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生成绩表;然后插入几行测试数据数据要尽量模拟真实情况INSERT INTO student_score (student_name, major, subject, score, exam_date) VALUES (张三, 计算机, MySQL, 88.5, 2024-01-10), (李四, 计算机, MySQL, 92.0, 2024-01-10), (王五, 软件工程, MySQL, 76.0, 2024-01-10), (张三, 计算机, Java, 81.0, 2024-01-20), (李四, 计算机, Java, 69.5, 2024-01-20), (王五, 软件工程, Java, 85.5, 2024-01-20), (赵六, 数据科学, MySQL, 90.0, 2024-03-15), (赵六, 数据科学, Python, 93.5, 2024-03-18);提示插入语句执行完后建议先跑一句SELECT * FROM student_score;确认数据已经写进去了。这个习惯非常重要我带过的新人里十个有八个SQL 查不到数据第一反应是问别人而不是先检查源数据。2.2 SELECT 基础框架你写的第一个查询DQL 最基础的语法长这样SELECT 列名1, 列名2 FROM 表名;SELECT后面跟你要看的列FROM后面跟你要查的表。如果你想把所有列都显示出来可以直接写星号*但真到了工作环境里我强烈不建议在代码里无脑用SELECT *尤其是在数据量大的生产库上。它会把所有列的数据都拉回来既浪费网络带宽又给数据库增加无谓的 IO 压力还会让你在代码评审时被同事点名。的准则是需要哪几列就写哪几列。-- 查询所有列学习时用 SELECT * FROM student_score; -- 只查询姓名和成绩两列工作中推荐 SELECT student_name, score FROM student_score;你还可以给查询出来的列起别名别名的作用是让结果集的表头变得清晰。语法是列名 AS 别名AS 也可以省略不写但写上更利于阅读SELECT student_name AS 姓名, score AS 成绩 FROM student_score;这段代码跑完结果表头会从英文列名变成中文“姓名”和“成绩”。别小看这个细节实际开发中你查出来的数据经常要对接给前端展示或者导出 Excel列名的可读性直接影响整个链路的沟通成本。2.3 SELECT 子句的执行顺序新手最容易忽略请务必在这一节就把执行顺序烂在脑子里因为这是你和“只会抄 SQL 的脚本小子”拉开差距的关键分水岭。标准 SELECT 语句的子句写起来有固定顺序但数据库真正执行的时候顺序和你写的完全不一样。以我们后续会用的复杂语句为例SELECT subject, AVG(score) AS avg_score FROM student_score WHERE score 60 GROUP BY subject HAVING AVG(score) 80 ORDER BY avg_score DESC LIMIT 3;这条语句的书写顺序是SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT。但在 MySQL 内部真正的执行顺序是这样的先执行FROM确定从哪张表取数据再执行WHERE对每一行原始数据进行逐行过滤把不满足条件的行扔掉接着执行GROUP BY把过滤后的行按指定列分组然后执行聚合函数比如AVG(score)是在分组之后才会计算的紧接着执行HAVING对分组后的聚合结果做二次过滤然后执行SELECT确定要输出哪些列计算别名再执行ORDER BY对最终结果排序最后执行LIMIT取出指定行数。强烈建议你现在就记住一个核心结论WHERE是先过滤行再分组HAVING是先分组再过滤组。后面写分组查询时这个区别就是决定你 SQL 正确与否的关键。3. 核心细节解析与实操要点3.1 WHERE 过滤把不需要的数据挡在门外WHERE的职责只有一个逐行判断条件留下满足条件的行。它的语法位置在FROM之后、GROUP BY之前。这里我给你整理一份最常用的过滤条件清单你在实际开发中几乎每天都会用到。条件类型运算符/关键字示例含义比较运算WHERE score 80成绩大于 80 分范围匹配BETWEEN ANDWHERE score BETWEEN 80 AND 90成绩在 80 到 90 之间含边界集合匹配INWHERE major IN (计算机,数据科学)专业属于集合内任意一个模糊匹配LIKEWHERE student_name LIKE 张%姓张的学生空值判断IS NULL/IS NOT NULLWHERE exam_date IS NOT NULL考试日期不为空逻辑连接ANDORNOTWHERE major计算机 AND score80计算机专业且成绩不低于 80这几种条件最核心的坑在于“空值判断”。新手经常写WHERE exam_date NULL这永远查不出任何数据。原因很简单NULL 不是值它代表“未知”。你不能用等号去和一个“未知”比较必须用IS NULL或IS NOT NULL。这是 SQL 世界里最经典的陷阱之一没有例外。LIKE模糊匹配也值得多说两句。%表示任意长度的任意字符_表示任意单个字符。比如LIKE 张%匹配所有“张”开头的字符串而LIKE 张_只匹配“张”后面带一个字的字符串。实际工作中LIKE %keyword%这种写法很影响性能因为它无法用到索引会在全表范围内做扫描数据量大了会明显变慢。3.2 去重与去空DISTINCT 和 NULL 的恩怨一张真实的数据表几乎必然存在重复数据。我们要去掉查询结果中的重复行最直接的方式是在SELECT后面加DISTINCTSELECT DISTINCT major FROM student_score;这条语句会把所有不重复的专业名称列出来。这里有个容易被忽略的重点DISTINCT是作用于整行的不是只作用于紧跟其后的那一列。比如SELECT DISTINCT major, subject FROM student_score;的意思不是“只给 major 去重”而是“major 和 subject 的组合不重复”的所有行。很多新手在这里理解错后面排错时总会多绕几圈。顺带一提DISTINCT在做统计时也很实用。比如想统计表中共有多少个专业可以写成SELECT COUNT(DISTINCT major) FROM student_score;这个写法能够直接算出“去重后”的数量比先查出去重列表再在程序里数一遍高效得多。3.3 排序与分页顺序和批次都安排明白ORDER BY负责排序后面可以跟列名、别名、甚至表达式。默认是升序ASC降序需要显式写DESC。-- 按成绩从高到低排序 SELECT student_name, score FROM student_score ORDER BY score DESC; -- 按多列排序先按专业升序同一专业内按成绩降序 SELECT student_name, major, subject, score FROM student_score ORDER BY major ASC, score DESC;多列排序时越靠前的排序字段优先级越高。上面的例子中专业是第一排序条件成绩是专业相同的情况下的第二排序条件。这种写法在生成报表时非常常见比如“按部门分组部门内部按业绩排名”。分页用的是LIMIT有两种写法。一种是只写一个参数LIMIT 5表示只返回前 5 条。更常用的是两个参数LIMIT 偏移量, 行数。偏移量从 0 开始表示跳过多少行。比如LIMIT 2, 3表示跳过前 2 行返回接下来的 3 行。另一种更清晰的写法是LIMIT 行数 OFFSET 偏移量逻辑完全一样。分页有个非常典型的坑如果不加ORDER BY分页结果的顺序在理论上是不可预期的。因为表本身是无序集合两次查询返回的行顺序可能不同。生产环境里做分页永远先写ORDER BY再写LIMIT这个习惯能帮你少掉无数头发。4. 实操过程与核心环节实现4.1 聚合函数五虎上将一次集齐聚合函数就是把多行数据聚合成一个值的函数。SQL 里最常用的有五个COUNT、SUM、AVG、MAX、MIN。它们经常配合GROUP BY使用但也可以单独使用单独使用时整个表会被当成一个组来看待。-- 统计总行数 SELECT COUNT(*) FROM student_score; -- 统计成绩总和 SELECT SUM(score) FROM student_score; -- 统计成绩平均值 SELECT AVG(score) FROM student_score; -- 统计最高分和最低分 SELECT MAX(score), MIN(score) FROM student_score;COUNT(*)统计的是所有行的数量包括列值为 NULL 的行。而COUNT(列名)统计的是该列“非 NULL”的行数。两者的差异在真实业务里经常会产生完全不同的统计口径。比如你统计一个订单表的发货时间列COUNT(*)算出来的是订单总数COUNT(ship_time)算出来的才是已发货订单数。多留个心眼别在生产表上直接用错。4.2 GROUP BY 分组把数据按维度切开分组是统计的灵魂。GROUP BY会把数据按某一列或某几列的值分成多个组然后每个组分别执行聚合函数。一个最常见的需求统计每个专业的平均成绩。SELECT major, AVG(score) AS avg_score FROM student_score GROUP BY major;执行逻辑是这样的先把所有行按major的值分堆“计算机”一堆、“软件工程”一堆、“数据科学”一堆然后分别计算每一堆的平均分。输出结果是每个专业一行每行带着自己的平均分。如果你在GROUP BY的场景里还想看具体某个学生的成绩那就错了——分组之后组内细节行已经被“折叠”成一组一行了。这也是为什么“分组后查询的列要么是分组字段要么是聚合函数计算结果”这条铁律如此重要。《只查询分组字段 聚合结果》这是 SQL 世界的金科玉律。MySQL 的默认配置在某些版本下允许你选择非分组字段而不报错但查出来的值是随机的纯属自欺欺人。分组也可以多维度组合。比如统计“每个专业、每门科目”的平均成绩SELECT major, subject, AVG(score) AS avg_score FROM student_score GROUP BY major, subject;这会把数据先按专业分再在专业内按科目细分输出结果就是一张专业与科目交叉的统计表。4.3 WHERE 与 HAVING 的分工很多人分不清WHERE和HAVING其实只需记住一条原则WHERE在分组之前过滤行HAVING在分组之后过滤组。场景举例我想查“平均成绩达到 85 分以上的专业”。注意平均成绩是分组之后才能算出来的过滤条件作用在“组”上所以必须用HAVINGSELECT major, AVG(score) AS avg_score FROM student_score GROUP BY major HAVING AVG(score) 85;换个场景我只想看“计算机和软件工程两个专业”的统计结果这个条件针对的是原始行数据不需要分组后再算那用WHERE就行SELECT major, AVG(score) AS avg_score FROM student_score WHERE major IN (计算机, 软件工程) GROUP BY major;到了复杂一点的场景WHERE和HAVING还可以配合使用。比如查“2024年1月份考试中平均成绩在 80 分以上的专业”SELECT major, AVG(score) AS avg_score FROM student_score WHERE exam_date BETWEEN 2024-01-01 AND 2024-01-31 GROUP BY major HAVING AVG(score) 80;这里的执行顺序是先用WHERE把 1 月份的数据筛出来再按专业分组最后用HAVING过滤掉平均分不及 80 的分组。一层套一层思路非常清晰。4.4 条件分支CASE WHEN 让查询更聪明实际业务中数据经常不能直接满足展示需求。比如考试成绩是数值但我们希望输出成“优秀”“及格”“不及格”这样的等级。这就需要用到CASE WHEN表达式。SELECT student_name, subject, score, CASE WHEN score 90 THEN 优秀 WHEN score 75 THEN 良好 WHEN score 60 THEN 及格 ELSE 不及格 END AS level FROM student_score;CASE语句从上到下依次判断遇到第一个满足条件的WHEN就返回对应的结果后面的分支不再判断。这意味着分支的顺序是有讲究的必须把范围更严格的放在前面否则会匹配错。比如你把score 60写在score 90前面90 分以上的学生会被错误地标记成“及格”。CASE WHEN还可以配合聚合函数做“条件计数”。比如统计每个专业中“成绩及格的人数”和“成绩不及格的人数”SELECT major, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) AS pass_cnt, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) AS fail_cnt FROM student_score GROUP BY major;这种写法非常实用可以一次性把多个口径的数据都统计出来避免了把同一张表反复 JOIN 的笨重操作。5. 常见问题与排查技巧实录5.1 语法没错但结果不对先看这几处我见过的 SQL 新手中的问题80% 集中在下面几类我把它整理成一张排查表你自己对照着走一遍。症状最可能的原因处理方式查询结果为空条件用比较 NULL改为IS NULL分组结果里少数据WHERE里用了聚合结果聚合条件移到HAVING分页数据顺序飘忽ORDER BY没写或者不唯一增加唯一列排序比如ORDER BY id查询结果有重复缺DISTINCT或表里真的有重复明确业务需求按具体情况去重排序数字变成文本一样列类型是字符串用CAST(列名 AS DECIMAL)转类型成绩为空统计不准AVG(score)忽略 NULL先确认业务上 NULL 是否参与计算AVG、SUM、MAX、MIN这几个聚合函数都会自动忽略 NULL 值。如果一个组里所有成绩都是 NULLAVG算出来还是 NULL而不是 0。这个特性在生成报表时经常引发问题你需要提前想清楚业务预期是什么如果需要把 NULL 当 0 处理可以用IFNULL(score, 0)提前转换。5.2 隐式类型转换查询变慢的隐形杀手MySQL 有一种“隐式类型转换”的特性就是当字段类型和查询条件类型不一致时数据库会偷偷把其中一个转成另一个再比较。听起来很贴心实则很容易引发灾难。比如你的id列是整数类型但你硬要用字符串去查SELECT * FROM student_score WHERE id 1;MySQL 能查出结果但问题在于如果id列上有索引隐式转换会导致索引失效强制走全表扫描性能成数量级下降。这种坑在真实生产环境里排查起来极其折磨人肉眼看着 SQL 没啥问题一查执行计划发现根本没走索引。所以一个必须养成的习惯是写条件时类型一定要和字段类型对齐。字符串列用引号包起来数值列不要加引号。日期字段也尽量用标准格式YYYY-MM-DD避免歧义。5.3 善用 EXPLAIN 看到执行计划当你的单表查询开始变慢并且已经排除了索引失效和全表扫描的可能我强烈建议你学会看执行计划。方法很简单在你的SELECT语句前面加一个关键词EXPLAINEXPLAIN SELECT student_name, score FROM student_score WHERE score 80;执行计划会告诉你几个关键信息最重要的当属type字段。你从最好到最差会依次看到const按主键查一行、ref走非唯一索引、ALL全表扫描。看到ALL就要提高警惕了说明你的查询没走到索引数据量一大性能就崩。key字段代表实际用到的索引名如果显示NULL说明该查询没有使用任何索引。rows字段是 MySQL 估计的扫描行数数值越大越危险。对一个单表查询初学者来说学会看这四列已经能解决大部分性能验证问题。提示不要在生产环境高峰期随便执行EXPLAIN太过复杂的查询虽然它不真正返回数据但某些情况下仍会消耗少量资源。日常开发库上随便用养成习惯比优化本身更重要。5.4 LIMIT 分页的深水区深分页小数据量时分页很丝滑一旦数据到百万级LIMIT 100000, 20这种写法就可能慢到让人怀疑人生。原因是 MySQL 需要把前 10 万条数据全部扫出来再丢弃掉只返回最后的 20 条。更高效的做法是“延迟关联”或者“游标分页”思路。简单说先用覆盖索引快速查到目标行的主键再用主键去关联查出完整列SELECT t.student_name, t.score FROM student_score t INNER JOIN ( SELECT id FROM student_score ORDER BY id LIMIT 100000, 20 ) tmp ON t.id tmp.id;或者在连续性主键场景下用WHERE id 上一次查询的最大id ORDER BY id LIMIT 20来翻页这种写法也叫 keyset pagination性能稳定得多。这块知识目前对你来说可能还属于拓展内容但先埋个种子等你实际遇到慢分页时再回头看会豁然开朗。6. 总结之外的一些实操心得学单表查询最朴素的方法就六个字多敲、多看、多想。多敲是指把这些示例语句亲自执行一遍不要只看不练多看是指拿到一个需求时先去观察原始数据长什么样再决定怎么写多想是指每写完一句 SQL自己追问一句“这个结果对业务来说合理吗”。我印象最深的一次经历是线上一个报表接口响应突然变慢排查到最后发现只是因为在 WHERE 条件里用了一个类型不一致的查询导致索引失效。从那之后我给自己定了一条规矩任何 SQL 写完后先看一眼执行计划再上线成本极低收益极高。另外关于学习资料官方文档始终是最准确的信息源。遇到不确定的语法优先去查 MySQL 官方文档。网上很多教程内容过时或者带有误导性尤其是一些聚合函数的行为细节、排序规则的边界情况请以你当前所用 MySQL 版本的行为为准。最后分享一个我一直在用的练习习惯准备一张几千行的业务模拟表每天给自己出 5 个查询需求逼着自己只用单表查询完成绝不上手就查百度。坚持两周你对 DQL 的熟练度会有一个质的飞跃。这份扎实的基本功会在你日后接触多表查询、复杂报表和性能优化时变成你最值钱的底牌。
返回列表