☰
MySQL查询操作全解析:从基础SELECT到性能优化实战
2026/10/9 8:46:28 网站建设 项目流程

做MySQL这一行,不管是刚装好环境的新手,还是已经接手过生产库的运维,每天打交道最多的就是查询操作。你可能会背一大堆安装命令、调优参数,但真正落到业务上,最先要写利索的永远是那句SELECT。今天这篇不聊安装,不讲架构,就专门把MySQL最基础的查询操作掰开揉碎讲一遍。我会用实际的学生成绩场景做贯穿,把常见的坑、容易搞混的语法、还有实际排查问题时的思路都过一过,保证你看完能直接把写法搬到自己库里跑。

我自己在带新人的时候说过很多次:查询写得好不好,不是看你会不会用LEFT JOIN,而是看你面对一个需求时,能不能用最简单的语句把结果取准确。复杂功能都是简单操作堆出来的,今天先把地基打牢。

1. 先弄明白查询到底在查什么

很多初学者上来就背SELECT 列名 FROM 表名 WHERE 条件,觉得这就是查询的全部。实际上一条查询语句背后,MySQL要经历的东西远比表面看到的复杂。我先带你把查询逻辑拆开看,这样后面写语句的时候,脑子里就有一张完整的执行流程图,不会迷迷糊糊试半天。

1.1 一条查询语句的解剖结构

一条完整的SQL查询,核心部分就是三大块:要哪几列、从哪张表来、按什么条件筛。对应到SQL语法上就是SELECT、FROM、WHERE这三个关键字。这是最基础的骨架。

SELECT 列名1, 列名2 FROM 表名 WHERE 筛选条件;

举个例子,有一张学生表student,你想查所有学生的姓名和年龄,就这么写:

SELECT name, age FROM student;

这里SELECT后面的name、age就是要取的列,FROM后面指定了表。没有WHERE就代表全表所有行都返回。加了WHERE才开始做行的过滤。

SELECT name, age FROM student WHERE age > 18;

不过你要是以为MySQL真的按SELECT、FROM、WHERE这个顺序执行,那就错了。我面试时候经常拿这个问题试探候选人:SQL的书写顺序和执行顺序不一样。MySQL真实的执行顺序大致是:先FROM,确定从哪张表取数据;然后WHERE,把不符合条件的行过滤掉;接着GROUP BY、HAVING做分组和分组后的过滤;再然后SELECT,确定要输出的列;最后ORDER BY排序、LIMIT做分页截断。这个顺序理解透以后,很多奇怪的问题都能解释清楚。比如你问为什么WHERE里不能用SELECT后面才定义的别名,就是因为执行到WHERE时,SELECT的别名根本还不存在。

1.2 用生活化类比拆解查询的思维

把查询操作类比成去超市买菜,你拿着购物清单进超市(FROM告诉你超市是哪家),在货架之间走动的时候,按照你的要求把不想要的商品直接跳过(WHERE过滤),最后把想要的商品装进购物车结账(SELECT输出结果)。如果你还想最后按价格高低排个序,那就是结账出门前再看一眼购物车,按价格摆整齐(ORDER BY)。

这个类比看着简单,但我想强调一个关键点:MySQL处理数据的单位是行,不是列。哪怕你SELECT只取了一列,它在内存里也是先把完整的行读出来,再一层层过滤,最后才把需要的列输出。所以写查询的时候要知道,WHERE条件用得越准确,MySQL提前过滤掉的行就越多,效率自然就高了。

这个阶段不用着急啃执行计划,先把“先过滤后取列”这个思维刻在脑子里。后面写复杂查询时,你只要记住MySQL最后才帮你把列整理出来,就不会写出满屏全表扫描的语句了。

2. 简单查询的常用姿势,直接抄

基础语法谁都会背,但实际写的时候,细节决定成败。这一节我把日常工作中用得最频繁的几种查询写法,连同容易出岔子的角落一起整理出来。每条我都给了正反例,你可以直接对照着抄。

2.1 单表查询的写法与WHERE条件细节

先看最简单的整表查询。开发环境里我常用来快速看数据:

SELECT * FROM student;

注意,生产环境我极不推荐用这个。SELECT *会把所有列都查出来,一旦表里有大字段(比如TEXT、BLOB),或者列数特别多,白白浪费IO和内存。更关键的是,如果哪天表结构加了列,你的程序拿到的结果集列数也会变,容易出隐患。正确做法是明确列出你需要的列。

WHERE条件里,等值判断、范围判断、模糊匹配是三类最常见的需求。

等值判断:

SELECT name, class_id FROM student WHERE student_id = 20240001;

范围判断用BETWEEN AND:

SELECT name, score FROM score WHERE score BETWEEN 80 AND 90;

这个写法等价于score >= 80 AND score <= 90,注意它是包含边界的。我见过很多同事把边界记成开区间,一查就漏数据。

模糊匹配用LIKE:

SELECT name FROM student WHERE name LIKE '张%';

%代表任意多个字符,_代表单个字符。所以张%查的是姓张的,张_查的是名字只有两个字的张姓学生。这里有个性能大坑:如果查询条件是LIKE '%张'或者LIKE '%张%',即使你在name列上建了索引,也没法走索引,MySQL只能全表扫。原因很简单,索引是按列值从前往后建立的,前缀不确定,索引就无从定位。能避免就避免,非用不可的时候再考虑全文索引或者其他方案。

WHERE里还可以用IN,表示匹配一个列表:

SELECT * FROM score WHERE course_id IN (1, 2, 3);

与之相对的是NOT IN,但有个坑我后面会专门讲:当列表里包含NULL时,NOT IN的结果会跟你想象的不一样。

2.2 排序和分页,最容易写错的两个地方

排序用ORDER BY。默认是升序ASC,降序要显式写DESC。多字段排序时,从左到右依次起作用。

SELECT name, score FROM score ORDER BY score DESC, name ASC;

这个语句的意思是先按score降序排,如果两条记录score一样,再按name升序排。这就解决了一个新手常犯的错:以为写两个ORDER BY字段就能分别控制方向,实际只能靠逗号分隔,每一列后单独写方向。

分页用LIMIT,这是面试和实际开发都高频出现的关键字。MySQL里分页的标准姿势:

SELECT * FROM score ORDER BY score DESC LIMIT 10 OFFSET 20;

意思是跳过前20行,从第21行开始取10行。也可以写成LIMIT 20, 10,逗号前面的数字是偏移量,后面是行数。这个顺序特别容易搞混,我建议统一用OFFSET写法,语义更清晰。

分页有个性能隐患,偏移量越大越慢。当你需要翻到第100000行时,MySQL依然要把前面99999行全部扫过才能取到目标数据。实际业务里我见过不少翻页翻到后面就卡死的场景,这时候要么限制最大翻页深度,要么改用“上一页最后一条记录的ID + WHERE”这种键集分页。不过这些都是后话,先把简单分页写对。

2.3 去重和条件组合,别把DISTINCT用错地方

去除重复行用DISTINCT:

SELECT DISTINCT class_id FROM student;

这个语句会返回所有不重复的class_id。注意,DISTINCT作用在SELECT后面所有列的组合上,不是只作用于紧跟的那一列。如果你写:

SELECT DISTINCT class_id, name FROM student;

返回的是class_id和name两列组合起来不重复的行,不是“只对class_id去重”。这个我在实际项目里踩过不只一次,同事拿它做班级列表去重,发现结果里同一个班出现好多次,就是因为组合去重的机制。

条件组合用AND、OR、NOT。AND优先级高于OR,所以想表达“A或B,且C”时,一定要加括号:

SELECT * FROM score WHERE (course_id = 1 OR course_id = 2) AND score >= 60;

这个语句查的是课程1或课程2中,成绩及格的记录。如果漏了括号,写成了WHERE course_id = 1 OR course_id = 2 AND score >= 60,实际含义就变成:课程1的所有记录,加上课程2中及格的记录。结果完全不一样。这不是小坑,生产环境我真见过因为漏括号查出错误数据的。

再提一个热词里的疑问:“mysql的or能去重吗”。这个问题的答案是不能,去重和OR压根不挨着。OR只是扩大筛选范围,只要满足任何一个条件,行就会被选中。如果同一行同时满足两个条件,结果集里也只会出现一次,因为查询返回的是行集合,不是条件命中的次数。这个去重效果是行集本身的特性,不是OR的功劳。

3. 拿来就用的实战场景:学生成绩查询

光列语法没意思,我用一个贯穿全文的实战例子把上面的知识点串起来。这个例子正好贴合实际项目里“学生课程成绩信息实体表设计mysql”的常见场景,也方便你直接建表跑一跑验证。

3.1 学生-课程-成绩三张表的准备

先建三张最基础的表:学生表、课程表、成绩表。

学生表:

CREATE TABLE student ( student_id INT PRIMARY KEY, name VARCHAR(50) NOT NULL, class_id INT DEFAULT 1, age TINYINT );

课程表:

CREATE TABLE course ( course_id INT PRIMARY KEY, course_name VARCHAR(50) NOT NULL );

成绩表:

CREATE TABLE score ( student_id INT, course_id INT, score DECIMAL(5,1), PRIMARY KEY (student_id, course_id) );

成绩表用联合主键,保证同一个学生同一门课只能有一条成绩。DECIMAL(5,1)的意思是总共5位数字,小数点后保留1位,最大能存9999.9,足够用了。我见过有人用FLOAT存分数,后来出现0.30000000000000004这种诡异结果,就是因为浮点数的二进制精度问题。金额、分数这种需要精确的值,直接用DECIMAL。

插入几条测试数据,方便后面查询:

INSERT INTO student VALUES (1, '张三', 1, 18), (2, '李四', 1, 19), (3, '王五', 2, 18), (4, '赵六', 2, 20); INSERT INTO course VALUES (1, '数学'), (2, '英语'), (3, '物理'); INSERT INTO score VALUES (1, 1, 88.5), (1, 2, 75.0), (2, 1, 92.0), (2, 3, 68.5), (3, 2, 81.0), (3, 3, 95.0), (4, 1, 43.5);

3.2 按分数段统计的完整写法

需求一:查出数学成绩大于等于80分的学生名字和分数。

SELECT s.name, sc.score FROM score sc JOIN student s ON sc.student_id = s.student_id WHERE sc.course_id = 1 AND sc.score >= 80;

这里我用了JOIN关联两张表,逻辑很简单:成绩表里先筛出课程1且分数达标的行,再关联到学生表拿名字。JOIN的用法后面会展开,你先感受一下查询从单表走向多表的过程。

需求二:统计每个班级的平均分、最高分、最低分。

SELECT st.class_id, AVG(sc.score) AS avg_score, MAX(sc.score) AS max_score, MIN(sc.score) AS min_score FROM score sc JOIN student st ON sc.student_id = st.student_id GROUP BY st.class_id;

GROUP BY把数据按class_id分组,然后对每组分别算AVG、MAX、MIN。这是查询操作里从“取数据”跨向“汇总统计”的关键一步。注意,SELECT后面除了聚合函数,只能出现GROUP BY里写过的列,这是SQL规范,MySQL有些版本默认放开了一部分,但写规范了总没错。

需求三:筛选出平均分大于80的班级,这时候要用HAVING而不是WHERE。

SELECT st.class_id, AVG(sc.score) AS avg_score FROM score sc JOIN student st ON sc.student_id = st.student_id GROUP BY st.class_id HAVING AVG(sc.score) > 80;

WHERE和HAVING的区别,是我在面试里必问的基础题。简单记:WHERE在分组之前过滤行,HAVING在分组之后过滤组。WHERE里不能用聚合函数,HAVING专门服务于聚合结果。

3.3 关联查询与子查询:从单表走向多表

日常业务很少真的只查一张表,至少也要关联出个名称。JOIN大致分三种:INNER JOIN、LEFT JOIN、RIGHT JOIN。我推荐你记住INNER和LEFT就够用了,RIGHT JOIN完全可以改写为LEFT JOIN的镜像,能少记一个就少记一个。

INNER JOIN只返回两边都匹配上的行:

SELECT s.name, c.course_name, sc.score FROM score sc JOIN student s ON sc.student_id = s.student_id JOIN course c ON sc.course_id = c.course_id;

LEFT JOIN返回左表全部行,右表没匹配上就补NULL:

SELECT s.name, sc.score FROM student s LEFT JOIN score sc ON s.student_id = sc.student_id;

这个语句能查出所有学生,包括没参加考试的学生,没考的那几门成绩就是NULL。搞懂INNER和LEFT的区别,就够覆盖九成以上的关联查询需求了。

子查询也是简单查询的延伸。比如查成绩表里有记录的学生:

SELECT name FROM student WHERE student_id IN ( SELECT DISTINCT student_id FROM score );

这种写法逻辑直观,但要注意子查询的结果集越大,性能越差。能用JOIN改写就尽量用JOIN,上面这个功能写成:

SELECT DISTINCT s.name FROM student s JOIN score sc ON s.student_id = sc.student_id;

效果一样,MySQL优化器执行起来通常更顺畅。初学者阶段不用过度担心两种写法性能差多少,但养成习惯,能用JOIN直连的就别套子查询,后面量起来了你就会感谢这个习惯。

4. 常见问题与排查技巧实录

查询写出来只是第一步,写出来的结果对不对、慢不慢才是真考验。这一节我整理几个实际运维和开发中高频出现的坑,每个都是我或者我身边同事真金白银踩出来的,你提前知道能省不少事。

4.1 查询结果怎么跟业务对不上

最典型的场景:明明表里有数据,查询却返回空。我先问一句,你确认那张表里的数据提交了吗?很多开发环境里,INSERT后忘了COMMIT,同一个连接里查能看到,程序里换一条连接就查不到,其实数据还在未提交状态。排查时先确认事务状态,尤其MySQL默认InnoDB是自动提交的,但如果你手动开启了事务,就得格外小心。

另一个常见原因是字符集和大小写。MySQL默认的排序规则下,字符串比较不区分大小写,WHERE name = 'zhangsan'能查到ZhangSan,但如果你当时建表时指定了utf8mb4_bin这种二进制排序规则,它就严格区分大小写,查不到很正常。遇到这种怪问题,先看表的字符集和排序规则。

还有一个最容易忽略的:WHERE条件里写错字段类型。比如字段是VARCHAR,你直接拿数字去查:

SELECT * FROM student WHERE age = '18';

这里age如果定义成VARCHAR,但你存的是'18',查询时MySQL可能做隐式转换,导致索引失效。更危险的是,如果字段是字符串类型,你拿数值去匹配,MySQL确实会转成数值后比较,但一旦转换出错,结果就是全表扫。排查这类问题,直接看EXPLAIN有没有走索引。

4.2 NULL值处理,一不留神就翻车

NULL是SQL世界里的三值逻辑怪物。查NULL不能用等于,要用IS NULL:

SELECT * FROM score WHERE score IS NULL;

反过来查非空用IS NOT NULL。千万不用score = NULL,这个条件永远返回空结果,因为你是在拿值跟“未知”做等值比较,结果永远也是“未知”,行不会被选中。

前面提到的NOT IN坑也在这儿。假如你想查没选任何课程的学生:

SELECT * FROM student WHERE student_id NOT IN (SELECT student_id FROM score);

如果score表里恰好有student_id为NULL的记录,那么这个NOT IN的结果会把你所有想查的学生都过滤掉。原因是NOT IN遇到NULL时,整个条件结果变成“未知”,一行也查不出来。解决办法是子查询里先排除NULL:

SELECT * FROM student WHERE student_id NOT IN ( SELECT student_id FROM score WHERE student_id IS NOT NULL );

或者干脆用NOT EXISTS,这个写法更稳,后面有机会再展开。记住一条:涉及NULL的判断,永远用IS NULL或者IS NOT NULL,别的写法都不靠谱。

4.3 误操作后的查询还原思路

热词里有人搜“mysql update 还原”,我虽然没有时光机,但可以分享一套应急思路。假设你刚执行了一条UPDATE没带WHERE,把整表字段都改了,先别慌,第一步是确认有没有开启binlog,有的话可以从binlog里找到误操作前的位置,用mysqlbinlog解析出原始值,再做反向修复。

如果没有binlog,唯一的希望是数据有没有备份。补救流程大概是:先停止业务写入,防止新数据污染现场;然后直接从备份文件把那张表恢复到误操作前的时间点;最后如果还有其他增量写入,再把binlog里的增量SQL过滤掉误操作的部分后重放。

这个场景的真话是什么?真话是平时把备份做好,比什么技巧都重要。查询操作练得再熟,也挡不住手滑。我给自己的原则就是:生产环境执行UPDATE、DELETE之前,先写SELECT确认要改哪些行,查出来看一眼,没问题再改。这条习惯记到肌肉里,能救你无数次。

5. 从简单查询走向性能优化

查询写正确了,第二步追求的是写得快。MySQL的查询性能问题,百分之九十都出在表扫描上。这一节我不展开讲调优全体系,只讲简单查询能直接用的两个抓手:EXPLAIN和索引。

5.1 用EXPLAIN给查询做体检

任何一条查询语句,前面加EXPLAIN,MySQL就会告诉你它打算怎么执行。我常用的是看type、key、rows这三个字段。

EXPLAIN SELECT name FROM student WHERE student_id = 1;

type字段如果出现ALL,说明是没走全表扫,表的数据量小还好,数据量一大就要警觉。出现ref或range通常代表走了索引,const代表等值命中主键,这是比较理想的。

key字段显示实际用到的索引名。如果显示NULL,说明没走索引。rows字段是MySQL估算要扫的行数,这个值越小越好。你把自己那条慢查询前面加个EXPLAIN,基本一眼就能定位问题是不是出在“全表扫”上。

第一次看EXPLAIN的人最容易犯的错是只看有没有走索引,不看扫描行数。有时候明明走了一个索引,但因为条件写得宽泛,扫的行数还是接近全表。真正有效的优化是让扫描行数降下来。

5.2 索引与简单查询的关系

索引的概念可以理解成一本书的目录。MySQL里最常见的索引类型是B+树,它把字段值排好序,查询时通过二分查找快速定位目标行。你在WHERE、JOIN条件里用到的字段,如果建了索引,查询效率通常会有质的提升。

拿前面的例子说,score表经常按student_id和course_id查,就非常适合建联合索引:

CREATE INDEX idx_student_course ON score(course_id, student_id);

这里我把course_id放在前面,因为业务查询通常先按课程过滤,再关联学生。联合索引有“最左前缀”原则:索引的字段顺序决定了它能匹配的条件组合。如果查询条件只包含第二个字段,这个索引就用不上。所以在设计索引顺序时,一定要优先考虑最常见的那组等值条件。

我见过不少人一听说索引能提速,就在每一列上都建一个,结果写入变慢、磁盘占用飙升,查询效率反而没提升多少。索引不是越多越好,它的本质是拿空间换时间。简单查询阶段,你只要记住:WHERE里常用的等值字段、JOIN的关联字段,值得建索引;频繁更新的字段、低选择度的字段(比如性别),建索引的意义不大。

要从简单查询往更深的水域走,方向大概就是这几条:多表关联的JOIN顺序优化、子查询改JOIN、聚合查询的思路、以及EXPLAIN的深入使用。我后面打算专门写一篇关于索引设计和慢查询优化的内容,可以先把这个坑占住。你在日常查询操作里遇到什么特别诡异的现象,也欢迎留言聊一聊,毕竟SQL这玩意儿,经验基本都是从踩坑里攒起来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询