开篇介绍:
hello 大家,那么在前面的学习中,我们学习了MySQL中的对数据库的操作,对表的操作,数据类型,那么我们前面也说了,我们遵循由外到内的学习路线,那么在学习完表的操作之后,比表还小的,也就是数据了,所以,接下来,我们就来学习一下MySQL中的,对于数据的操作,增删查改,这一部分知识点,可以说是最重要最核心的一个知识点了,所以各位,come on!
哈哈,OK,那么接下来,我们就来学习学习。
其实 MySQL 的核心操作 ——CRUD(Create 新增、Retrieve 查询、Update 更新、Delete 删除),本质上就是对 “数据库表” 的 “增、查、改、删”,就像我们日常操作 Excel 表格一样,只是换了一套更规范、更高效的 “语言”。
一、CRUD 核心概念:先搞懂 “到底在做什么”
在正式讲解语法前,我们先把底层逻辑说透 ——CRUD 是 4 个英文单词的缩写,对应 MySQL 中对数据表的四类核心操作,也是所有数据库操作的基础,没有之一。
可以用一个非常生活化的类比来理解:MySQL 的 “数据表” 就相当于我们电脑里的 Excel 工作表,CRUD 就是对这个工作表的四种核心操作:
- Create(新增):对应 Excel 里的 “新增一行数据”,比如给学生表添加一条新学生的记录、给成绩表填一条刚考完的考试分数;
- Retrieve(查询):对应 Excel 里的 “筛选数据、排序数据、计算数据”,这是日常使用频率最高的操作,比如查 “语文不及格的学生名单”“全班总分前 3 的同学排名”“每个学生的平均分”;
- Update(更新):对应 Excel 里的 “修改单元格内容”,比如把学生的数学成绩从 78 分改成 80 分、把员工的部门从 “销售部” 调整到 “市场部”、把过期的手机号改成新号码;
- Delete(删除):对应 Excel 里的 “删除整行数据”,比如删除已退学学生的记录、清理过期的考试成绩、移除重复的用户信息。
为什么不用 Excel 而用 MySQL?核心原因有三个:
二、Create(新增数据):把数据 “填” 进表里
新增数据是和数据库交互的第一步 —— 没有数据,后续的查询、更新、删除都无从谈起。MySQL 中新增数据的核心语法是INSERT
2.1 新增数据的核心语法结构
先记住INSERT的通用语法框架,后续所有新增场景都是这个框架的变形,没有例外:
INSERT [INTO] 表名 [(列1, 列2, 列3, …)] VALUES (值1, 值2, 值3, …) [, (值1, 值2, 值3, …)] …
我们把这个语法拆成 “人话版”,每个部分都解释清楚,确保你看到就懂:
- INSERT [INTO]:这是固定的关键字组合,作用是 “告诉数据库,我要往表里填数据了”。这里的INTO可以省略,比如写成INSERT 表名 VALUES …,效果完全一样,但加上INTO会更易读,新手建议加上;
- 表名:必填项,指定你要往哪张表里填数据,比如students(学生表)、exam_result(成绩表)、employees(员工表),必须是已经创建好的表,否则会报错;
- (列1, 列2, 列3, …),列其实指的就是字段,列1等待,其实就是要填写字段名:可选项,指定你要给哪些列赋值。如果不写这个部分,默认就是给表中所有列赋值,顺序必须和表创建时的列顺序一致;
- VALUES (值1, 值2, 值3, …):必填项,指定你要填的具体数据。每组值用括号括起来,多组值之间用逗号分隔,这样就能一次性插入多行数据,注意插入数据的类型要和对应的字段的数据类型相匹配哦;
- 结尾的分号;:MySQL 语句的结束标志,必须加,否则数据库不知道你这句话说完了。
大家看完之后自己再写一遍:insert into 表名字 (字段名,……) values (数据,……);
2.2 场景 1:单行数据 + 全列插入
2.2.1 适用场景
这种方式适合两种情况:一是临时手动补录单条数据,比如给学生表加一条新学生的信息;二是测试表结构是否正常,比如创建完表后插一条数据看看能不能成功。
2.2.2 语法 & 完整示例
首先,我们需要一张测试表,比如创建一张 “学生表”,包含 4 个列:
- id:学生 ID,整数类型,无符号(不能是负数),主键(唯一标识每条记录),自增(插入时不用手动赋值,数据库自动生成);
- sn:学号,整数类型,非空(必须填),唯一(不能重复);
- name:姓名,字符串类型,长度 20,非空;
- qq:QQ 号,字符串类型,长度 20,可空(可以不填)。
创建表的代码如下:
CREATE TABLE students (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
sn INT NOT NULL UNIQUE COMMENT '学号',
name VARCHAR(20) NOT NULL COMMENT '学生姓名',
qq VARCHAR(20) COMMENT 'QQ号'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

现在,我们要往这张表里插入一条 “孙悟空” 的完整信息,用全列插入的语法:
INSERT INTO students VALUES (101, 10001, '孙悟空', '11111');
执行这条语句后,数据库会返回一行提示:Query OK, 1 row affected (0.03 sec),意思是 “执行成功,影响了 1 行数据,耗时 0.03 秒”。

2.2.3 关键注意点
全列插入的约束最多,稍微不注意就会报错,这里总结了 4 个核心注意点:
INSERT INTO students VALUES (100, 10000, '唐三藏', NULL);
2.3 场景 2:多行数据 + 指定列插入
2.3.1 适用场景
这是实际开发中使用频率最高的插入方式,没有之一。它适合三种核心场景:
- 批量插入多条数据,比如一次导入 100 条学生信息,比单条插入快 10 倍以上;
- 不需要给所有列赋值,比如自增列(id)、默认值列、可空列,都可以省略;
- 表结构后续可能变更,比如新增一列 “年龄”,只要新增列允许为空,原有插入语句不用修改就能继续使用。
2.3.2 语法 & 完整示例
还是用学生表举例,我们要同时插入 “曹孟德” 和 “孙仲谋” 两条记录,并且只给id、sn、name三个列赋值(qq列暂不填),语法如下:
INSERT INTO students (id, sn, name) VALUES
(102, 20001, '曹孟德'),
(103, 20002, '孙仲谋');
执行后,数据库会返回提示:Query OK, 2 rows affected (0.01 sec),意思是 “执行成功,影响了 2 行数据”。

如果我们不想手动给id列赋值(利用自增特性),可以直接省略id列,语法更简单:
INSERT INTO students (sn, name) VALUES
(20003, '刘玄德'),
(20004, '诸葛亮');
执行这条语句后,数据库会自动给id列生成值:104 和 105(因为之前的最大 id 是 103),完美利用了自增列的特性。

2.3.3 核心优势 & 避坑要点
这种插入方式的优势太明显了
核心优势
避坑要点
2.4 场景 3:插入否则更新(解决 “主键 / 唯一键冲突”,避免报错)
2.4.1 问题背景:为什么会冲突?
这是新手插入数据时最常遇到的报错:ERROR 1062 (23000): Duplicate entry '100' for key 'PRIMARY',翻译过来就是 “主键冲突,100 这个值已经存在了”。
冲突的根源只有两个:
- 主键冲突:主键列的值必须唯一,比如学生表的id列是主键,已经插入了 id=100 的记录,再插一次 id=100 就会冲突;
- 唯一键冲突:唯一键列的值也必须唯一,比如学生表的sn列是唯一键,已经插入了 sn=20001 的记录,再插一次 sn=20001 就会冲突。
这种冲突在实际开发中太常见了:比如用户重复提交表单、批量导入数据时包含重复记录、定时任务同步数据时重复执行。
我们需要的不是报错,而是 “有则更新,无则插入” 的逻辑 —— 这就是INSERT … ON DUPLICATE KEY UPDATE的核心作用。
2.4.2 语法 & 完整示例
核心语法是在普通INSERT语句后面,加上ON DUPLICATE KEY UPDATE子句,指定冲突时要更新的列:
INSERT INTO students (id, sn, name) VALUES (100, 10010, '唐大师')
ON DUPLICATE KEY UPDATE sn = 10010, name = '唐大师';
我们来拆解这条语句的逻辑:
2.4.3 如何判断执行结果?(看受影响行数)
执行INSERT … ON DUPLICATE KEY UPDATE后,通过 “受影响行数” 可以精准判断执行结果,这对程序开发非常重要,我们总结了 3 种情况:


其实语法还是很好记住的,前面就是和普通的插入语句一样,只是要在插入语句后面加上
on duplicate key update 字段名=数据,(注意,一定要有逗号分割)……
2.4.4 核心优势:原子操作,避免并发问题
这里要补充一个非常重要的知识点:INSERT … ON DUPLICATE KEY UPDATE是原子操作。
什么是原子操作?简单说就是 “要么全部成功,要么全部失败”,不会出现 “一半成功一半失败” 的情况。
举个例子:如果不用这个语法,你需要先执行SELECT语句查有没有冲突数据,再执行INSERT或UPDATE。但在高并发场景下(比如 1000 人同时提交表单),可能会出现 “你查的时候没有,插入的时候突然有了” 的情况,导致报错。
而用INSERT … ON DUPLICATE KEY UPDATE,这个过程由数据库内部完成,是原子操作,完全避免了并发冲突问题 —— 这也是它在实际开发中被广泛使用的核心原因。
2.5 场景 4:替换(REPLACE,删除原有数据后插入)
2.5.1 适用场景
这种插入方式的使用场景比较特殊,适合 “需要完全替换冲突数据” 的情况。比如你确定要删除原有记录,用新记录替代它,并且接受 “未被覆盖的列会变成 NULL 或默认值” 的后果。
2.5.2 语法 & 完整示例
REPLACE的语法和INSERT几乎一样,只是把INSERT换成REPLACE:
REPLACE INTO students (sn, name) VALUES (20001, '曹阿瞒');
我们来拆解这条语句的逻辑:
执行后,数据库会返回提示:Query OK, 2 rows affected (0.05 sec)—— 这里的 “2 rows affected” 代表 “删除了 1 行,插入了 1 行”。

2.5.3 REPLACE vs INSERT … ON DUPLICATE KEY UPDATE(核心区别)
这两种方式都能解决冲突,但核心逻辑完全不同,很容易混淆。我们用一张表总结它们的 5 个核心区别:
| 冲突处理逻辑 | 先删除原有记录,再插入新记录 | 直接更新原有记录,不删除 |
| 未覆盖列的处理 | 未赋值的列会变成 NULL 或默认值 | 未更新的列保持原有值不变 |
| 受影响行数 | 冲突时返回 2(删 1 行 + 插 1 行) | 冲突时返回 2(更新 1 行) |
| 自增列的影响 | 会生成新的自增值(因为删除后重新插入) | 自增值保持不变(只是更新) |
| 适用场景 | 完全替换整条记录,不怕丢失数据 | 部分更新记录,保留原有数据 |
避坑警告:REPLACE会删除原有记录,导致未被覆盖的列数据丢失(比如qq列变成 NULL),生产环境务必慎用!除非你明确知道自己要做什么,否则优先用INSERT … ON DUPLICATE KEY UPDATE。
2.6 新增数据的额外避坑技巧
我们再补充 3 个容易踩的坑,以及对应的解决方案:
三、Retrieve(查询数据):MySQL 最核心的操作,没有之一
查询是 MySQL使用频率最高、语法最灵活、功能最强大的操作,没有之一。从简单的 “查全班成绩” 到复杂的 “查近 3 年每个月的销售额排名”,都属于查询的范畴。
我们可以把查询的过程比作 “筛豆子”:
- 表就是 “装豆子的筐”,里面有各种各样的豆子(数据);
- FROM子句就是 “选筐”,指定你要筛哪个筐里的豆子;
- WHERE子句就是 “筛子”,指定你要留下什么样的豆子(符合条件的数据);
- SELECT子句就是 “挑豆子”,指定你要挑哪些豆子(列)出来,还能对豆子进行加工(表达式计算);
- ORDER BY子句就是 “排豆子”,把挑出来的豆子按大小、颜色排序;
- LIMIT子句就是 “装豆子”,只装你需要的前 N 个豆子,避免豆子太多拿不动。
3.1 查询的核心语法结构
先记住SELECT的通用语法框架,这是一个 “万能框架”,后续所有查询都是这个框架的扩展,没有例外:
SELECT [DISTINCT] {* | 列1 [AS 别名1], 列2 [AS 别名2], …}
FROM 表名1 [别名1]
[JOIN 表名2 [别名2] ON 连接条件] — 多表查询用,新手暂时可忽略
[WHERE 筛选条件]
[GROUP BY 分组列1, 分组列2, …]
[HAVING 分组筛选条件]
[ORDER BY 排序列1 [ASC/DESC], 排序列2 [ASC/DESC], …]
[LIMIT 分页参数];
我们还是拆解每个部分:
- SELECT:必填项,指定你要查询的列或表达式,是整个查询语句的核心;
- DISTINCT:可选项,作用是 “去重”,去掉查询结果中的重复记录;
- *:代表 “查询所有列”,新手常用,但实际开发中尽量少用;
- AS 别名:可选项,给列或表达式起一个通俗易懂的名字,比如把chinese + math + english起名为总分;
- FROM 表名:必填项,指定你要查询的表,是查询的 “数据来源”;
- WHERE:可选项,筛选符合条件的行,是 “数据过滤的第一道关卡”;
- GROUP BY:可选项,按指定列分组,用于聚合统计(比如按部门分组统计平均工资);
- HAVING:可选项,对分组后的结果进行筛选,是 “数据过滤的第二道关卡”;
- ORDER BY:可选项,对查询结果排序,没有这个子句,结果顺序是未定义的;
- LIMIT:可选项,分页查询,只返回指定数量的记录,避免查询全表数据导致数据库卡死。
上面说的很重要,很重要!!!大家要牢记。
3.2 基础查询:查什么、从哪查
基础查询是所有复杂查询的基础
我们先创建一个成绩表,用于下面的示例:
— 创建表结构
CREATE TABLE exam_result (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL COMMENT '同学姓名',
chinese float DEFAULT 0.0 COMMENT '语文成绩',
math float DEFAULT 0.0 COMMENT '数学成绩',
english float DEFAULT 0.0 COMMENT '英语成绩'
);
— 插入测试数据
INSERT INTO exam_result (name, chinese, math, english) VALUES
('张伟', 85, 92, 78),
('李静', 79, 86, 88),
('王强', 91, 77, 69),
('刘芳', 88, 81, 90),
('陈杰', 72, 68, 75),
('杨敏', 83, 94, 82),
('黄伟', 66, 79, 67),
('赵娜', 93, 88, 91);

3.2.1 全列查询(SELECT *):尽量少用,只适合临时调试
语法 & 示例
全列查询的语法最简单,就是用*代表所有列:
SELECT * FROM exam_result;
执行这条语句后,数据库会返回exam_result表中的所有列和所有行数据,比如学生的 ID、姓名、语文、数学、英语成绩。

为什么不推荐使用?
很多新手刚学 MySQL 时,最爱用SELECT *,觉得方便快捷,但在实际开发中,强烈不推荐使用,原因有 4 个:
适用场景:仅适合两种情况 —— 一是临时调试,快速查看表中的数据;二是查询表的所有列(这种场景在实际开发中极少)。
3.2.2 指定列查询(推荐使用,性能最优)
语法 & 示例
指定列查询的语法是 “SELECT 列1, 列2, … FROM 表名”,比如查询成绩表中的 “姓名” 和 “英语成绩”:
SELECT name, english FROM exam_result;
执行后,数据库只会返回name和english两列数据,简洁高效。

核心优势 & 小技巧
这种查询方式的优势我们在前面已经提过,这里补充 2 个新手必知的小技巧:
3.2.3 表达式查询:对列数据进行 “加工处理”
有时候我们需要的不是列的原始值,而是基于列的计算结果,这就是 “表达式查询” 的核心作用。表达式可以是 “无字段表达式”“单字段表达式”“多字段表达式”,我们分别讲解。
场景 1:无字段表达式(和列无关,适合测试 / 标识)
无字段表达式就是 “不包含任何列的表达式”,比如固定值、数学公式,适合测试或给结果加标识。
示例:查询成绩表中的 “姓名” 和 “固定值 10”,语法如下:
SELECT name, 10 FROM exam_result;
执行后,每条记录都会多出一列值为 10 的数据。这种方式在实际开发中主要用于测试,比如验证查询是否能正常返回结果。

场景 2:单字段表达式(基于单个列计算,适合简单处理)
单字段表达式就是 “只包含一个列的表达式”,比如给英语成绩加 10 分、乘以 2,适合对单个列进行简单加工。
示例 1:查询姓名和 “英语成绩加 10 分” 的结果,语法如下:
SELECT name, english + 10 FROM exam_result;
示例 2:查询姓名和 “英语成绩乘以 2” 的结果,语法如下:
SELECT name, english * 2 FROM exam_result;

场景 3:多字段表达式(基于多个列计算,实战最常用)
多字段表达式就是 “包含多个列的表达式”,比如计算总分、平均分,这是实际开发中最常用的表达式类型。
示例:查询姓名和 “总分(语文 + 数学 + 英语)”,语法如下:
SELECT name, chinese + math + english FROM exam_result;
执行后,数据库会自动计算每个学生的总分并返回,比在程序里计算效率高 10 倍以上 —— 因为数据库是专门做数据计算的,比程序更擅长这个工作。

避坑要点:NULL 参与表达式的结果是 NULL
这里要补充一个非常重要的知识点:如果表达式中的列值包含NULL,那么整个表达式的结果都是NULL。
比如某个学生的english成绩是NULL(未考),那么chinese + math + english的结果就是NULL,而不是chinese + math。


解决方案:用IFNULL函数把NULL转换成 0,语法如下:
SELECT name, chinese + math + IFNULL(english, 0) FROM exam_result;

这样,即使english成绩是NULL,也会被当作 0 计算,总分结果就正常了。
3.2.4 别名:给查询结果 “起个好记的名字”
问题背景:为什么需要别名?
用表达式查询时,结果列的名称会是 “表达式本身”,比如chinese + math + english,非常不直观。别人看你的查询结果,根本不知道这列代表什么意思。
这时候,“别名” 就派上用场了 —— 给列或表达式起一个通俗易懂的名字,比如 “总分”“平均分”“英语加分后”,可读性直接翻倍。
语法 & 示例
别名的语法很简单:SELECT 列/表达式 AS 别名 FROM 表名,其中AS可以省略,直接写别名即可。
示例 1:给总分表达式起别名 “总分”,语法如下:
SELECT name, chinese + math + english AS 总分 FROM exam_result;
那么我们起别名时所写的字符串,是可以不用加‘’单引号的哦。
示例 2:省略AS,直接写别名,语法更简洁:
SELECT name, chinese + math + english 总分 FROM exam_result;

执行后,结果列的名称会变成 “总分”,一眼就能看懂。
核心注意点:别名不能用在 WHERE 子句中
这是新手查询时最常遇到的报错:Unknown column '总分' in 'where clause',意思是 “WHERE 子句中找不到‘总分’这个列”。
为什么会这样?因为 MySQL 的执行顺序是:先执行 WHERE 子句筛选数据,再执行 SELECT 子句计算别名显示。
这一点我们要严格注意哦。
当执行 WHERE 子句时,“总分” 这个别名还没有被计算出来,所以数据库不知道你说的 “总分” 是什么,自然会报错,那么最后显示出结果时,还是会使用别名,这一点大家需要注意一下。
正确的做法是:在 WHERE 子句中使用原始表达式,而不是别名。比如查询 “总分 < 200 的学生”,正确语法是:
SELECT name, chinese + math + english 总分 FROM exam_result WHERE chinese + math + english < 200;

虽然写起来麻烦,但这是唯一正确的方式。不过别担心,别名可以用在ORDER BY子句中,因为ORDER BY的执行顺序在SELECT之后。
3.2.5 去重查询:用DISTINCT去掉重复记录
适用场景
当查询结果中有大量重复记录时,用DISTINCT可以快速去掉重复记录,只保留唯一值。比如查询成绩表中的 “数学成绩”,去掉重复的分数。
语法 & 示例

示例 1:查询所有数学成绩,包含重复值:
SELECT math FROM exam_result;
执行后,结果中会有重复的 98 分(黄伟和小明都是 79 分)。

示例 2:用DISTINCT去重,查询唯一的数学成绩:
SELECT DISTINCT math FROM exam_result;

执行后,重复的 79 分只会出现一次,结果更简洁。
核心注意点:DISTINCT作用于所有指定列(不是单个列)
新手很容易误以为DISTINCT只作用于 “紧跟它的列”,但实际上,DISTINCT作用于所有指定列的组合。
比如查询DISTINCT name, math,数据库会判断 “姓名 + 数学成绩” 的组合是否重复,而不是只判断姓名是否重复。
示例:查询 “姓名 + 数学成绩” 的唯一组合:
SELECT DISTINCT name, math FROM exam_result;

即使两个学生的数学成绩相同,但姓名不同,也会被视为不同的记录,不会被去重。
3.3 WHERE 条件:筛选数据的 “黄金筛子”
WHERE子句是查询的核心灵魂,它的作用是 “筛选出符合条件的行数据”,就像一个黄金筛子,只留下你需要的豆子。
要用好WHERE子句,必须先掌握 “运算符”—— 这是筛选条件的基础。我们把运算符分为 “比较运算符” 和 “逻辑运算符” 两大类
3.3.1 比较运算符:判断 “数据是否符合条件”
比较运算符是用来判断 “一个值是否符合某个条件” 的符号,比如 “大于”“小于”“等于”“在某个范围内”。:
| > | 大于 | 筛选大于某个值的数据 | WHERE english > 80(英语成绩大于 80 分) |
| >= | 大于等于 | 筛选大于等于某个值的数据 | WHERE math >= 90(数学成绩大于等于 90 分) |
| < | 小于 | 筛选小于某个值的数据 | WHERE chinese < 60(语文成绩小于 60 分) |
| <= | 小于等于 | 筛选小于等于某个值的数据 | WHERE english <= 100(英语成绩小于等于 100 分) |
| = | 等于 | 筛选等于某个值的数据 | WHERE name = '孙悟空'(姓名是孙悟空) |
| !=/<> | 不等于 | 筛选不等于某个值的数据 | WHERE math != 98(数学成绩不是 98 分) |
| BETWEEN a AND b | 在 [a,b] 范围内(包含 a 和 b) | 筛选区间内的数据 | WHERE chinese BETWEEN 80 AND 90(语文 80-90 分) |
| IN (值1, 值2, …) | 是括号中的任意一个值 | 筛选多个离散值的数据 | WHERE math IN (58, 59, 98, 99)(数学成绩是这四个值之一) |
| IS NULL | 是 NULL 值 | 筛选空值数据 | WHERE qq IS NULL(QQ 号为空的学生) |
| IS NOT NULL | 不是 NULL 值 | 筛选非空值数据 | WHERE qq IS NOT NULL(有 QQ 号的学生) |
| LIKE | 模糊匹配 | 筛选符合模糊规则的数据 | WHERE name LIKE '孙%'(姓孙的学生) |
我们对其中最常用、最容易出错的运算符进行详细讲解
重点 1:BETWEEN a AND b—— 区间筛选,简洁高效
BETWEEN a AND b的作用是 “筛选出大于等于 a 且小于等于 b 的数据”,等价于>=a AND <=b,但语法更简洁。
示例:查询语文成绩在 80-90 分之间的学生,两种写法对比:
- 用AND:SELECT name, chinese FROM exam_result WHERE chinese >= 80 AND chinese <= 90;
- 用BETWEEN:SELECT name, chinese FROM exam_result WHERE chinese BETWEEN 80 AND 90;

两种写法的结果完全一致,但BETWEEN的语法更简洁,可读性更强。
避坑要点:BETWEEN a AND b中的a必须小于等于b,否则会返回空结果。比如WHERE chinese BETWEEN 90 AND 80,不会报错,但结果为空。
重点 2:IN (值1, 值2, …)—— 多值筛选,替代多个 OR
IN的作用是 “筛选出值等于括号中任意一个的数据”,等价于多个OR的组合,但语法更简洁,效率更高。
示例:查询数学成绩是 58、59、98、99 分的学生,两种写法对比:
- 用OR:SELECT name, math FROM exam_result WHERE math=58 OR math=59 OR math=98 OR math=99;
- 用IN:SELECT name, math FROM exam_result WHERE math IN (58,59,98,99);

当需要筛选的值超过 3 个时,IN的优势非常明显,不仅写起来快,数据库的执行效率也更高。
避坑要点:IN括号中的值类型必须和列类型一致。比如math是整数类型,括号中不能写字符串(比如'98'),虽然 MySQL 会自动转换,但可能导致索引失效,降低查询效率。
重点 3:LIKE—— 模糊匹配,查 “姓孙的学生”“名字带小的学生”
LIKE是模糊匹配的核心运算符,它的关键是两个通配符:%和_,我们分别讲解它们的作用:
- %:匹配任意多个字符(包括 0 个字符),比如'孙%'匹配 “孙悟空”“孙权”“孙” 等所有以 “孙” 开头的字符串;
- _:匹配严格的一个字符,比如'孙_'只匹配 “孙权” 这种两个字的名字,不匹配 “孙悟空”(三个字)。
我们用 4 个经典示例,把LIKE的用法讲透:
SELECT name FROM exam_result WHERE name LIKE '孙%';
结果会返回 “孙悟空” 和 “孙权”;
SELECT name FROM exam_result WHERE name LIKE '%谋';
结果会返回 “孙仲谋”;
SELECT name FROM exam_result WHERE name LIKE '%悟%';
结果会返回 “孙悟空” 和 “猪悟能”;
SELECT name FROM exam_result WHERE name LIKE '孙_';
结果只会返回 “孙权”,不会返回 “孙悟空”。
避坑要点:查询含%或_的字符串,需要用转义字符\\。比如查询名字带%的学生,语法是WHERE name LIKE '%\\%%',其中\\%代表 “真实的 % 字符”,而不是通配符。
重点 4:= vs <=>——NULL 值的判断
这是新手最容易混淆的两个运算符,核心区别是 “是否支持 NULL 值判断”。
- =:NULL 不安全,无法判断 NULL 值。比如NULL = NULL的结果是NULL(不是 TRUE),1 = NULL的结果也是NULL;
- <=>:NULL 安全,可以判断 NULL 值。比如NULL <=> NULL的结果是TRUE,1 <=> NULL的结果是FALSE。
我们用两个示例对比它们的区别:
SELECT name FROM students WHERE qq = NULL;
执行后,即使有学生的qq是 NULL,也不会返回任何结果;
SELECT name FROM students WHERE qq <=> NULL;
执行后,会返回所有qq为 NULL 的学生。
最佳实践:判断 NULL 值时,优先用IS NULL或IS NOT NULL,比<=>更易读。比如WHERE qq IS NULL,一眼就能看懂是 “查询 QQ 号为空的学生”。
3.3.2 逻辑运算符:组合多个条件(复杂筛选的核心)
逻辑运算符是用来 “组合多个比较条件” 的符号,比如 “语文 > 80 且数学 > 80”“英语 < 60 或语文 < 60”“不是姓孙的学生”。
常用的逻辑运算符有三个:AND、OR、NOT,我们用一张表总结它们的作用:
| AND | 多个条件都满足才返回 TRUE | 高 | WHERE chinese > 80 AND math > 80(语文和数学都大于 80 分) |
| OR | 多个条件任意一个满足就返回 TRUE | 低 | WHERE english < 60 OR chinese < 60(英语或语文不及格) |
| NOT | 对条件取反,TRUE 变 FALSE,FALSE 变 TRUE | 最高 | WHERE name NOT LIKE '孙%'(不是姓孙的学生) |
核心注意点 1:逻辑运算符的优先级(用括号改变顺序)
逻辑运算符的优先级是:NOT > AND > OR。这意味着,当一个筛选条件中同时包含AND和OR时,数据库会先计算AND的条件,再计算OR的条件。
比如条件WHERE chinese > 80 OR math > 80 AND english > 80,数据库会先计算math > 80 AND english > 80,再和chinese > 80做OR运算。
如果你的需求是 “语文> 80 或数学 > 80,并且英语 > 80”,必须用括号改变优先级,语法如下:
WHERE (chinese > 80 OR math > 80) AND english > 80;
括号的优先级最高,数据库会先计算括号内的OR条件,再计算括号外的AND条件,确保结果符合你的预期。
核心注意点 2:AND和OR的短路特性(提升查询效率)
逻辑运算符有 “短路特性”,这是提升查询效率的关键:
- AND短路:如果第一个条件为 FALSE,第二个条件不会被计算。比如WHERE 1=2 AND chinese > 80,因为1=2是 FALSE,数据库不会计算chinese > 80,直接返回 FALSE;
- OR短路:如果第一个条件为 TRUE,第二个条件不会被计算。比如WHERE 1=1 OR chinese > 80,因为1=1是 TRUE,数据库不会计算chinese > 80,直接返回 TRUE。
在实际开发中,把 “筛选结果少” 的条件放在AND前面,把 “筛选结果多” 的条件放在OR前面,可以显著提升查询效率。
3.3.3 经典筛选场景实战
理论讲得再多,不如实战来得实在。我们总结了 10 个经典的筛选场景。
| 1. 英语不及格的学生 | 英语成绩 < 60 | SELECT name, english FROM exam_result WHERE english < 60; |
| 2. 语文 80-90 分的学生 | 语文成绩在 80 到 90 之间 | SELECT name, chinese FROM exam_result WHERE chinese BETWEEN 80 AND 90; |
| 3. 数学是 98 或 99 分的学生 | 数学成绩在 (98,99) 中 | SELECT name, math FROM exam_result WHERE math IN (98,99); |
| 4. 姓孙的学生 | 姓名以孙开头 | SELECT name FROM exam_result WHERE name LIKE '孙%'; |
| 5. 语文 > 英语的学生 | 语文成绩 > 英语成绩 | SELECT name, chinese, english FROM exam_result WHERE chinese > english; |
| 6. 总分 < 200 的学生 | 语文 + 数学 + 英语 < 200 | SELECT name, chinese+math+english 总分 FROM exam_result WHERE chinese+math+english < 200; |
| 7. 语文 > 80 且不姓孙的学生 | 语文 > 80 且 姓名不是孙开头 | SELECT name, chinese FROM exam_result WHERE chinese > 80 AND name NOT LIKE '孙%'; |
| 8. 名字是两个字的学生 | 姓名长度为 2(用 CHAR_LENGTH 函数) | SELECT name FROM exam_result WHERE CHAR_LENGTH(name) = 2; |
| 9. 有 QQ 号的学生 | QQ 号不为 NULL | SELECT name, qq FROM students WHERE qq IS NOT NULL; |
| 10. 总分 > 200 且语文 < 数学的学生 | 总分 > 200 且 语文 < 数学 | SELECT name, chinese, math FROM exam_result WHERE (chinese+math+english) > 200 AND chinese < math; |

3.4 ORDER BY:给查询结果 “排个序”(让数据更有规律)
默认情况下,查询结果的顺序是 “未定义” 的 —— 数据库怎么存储数据,就怎么返回数据,完全没有规律。
而ORDER BY子句的作用,就是 “给查询结果按指定规则排序”,让数据更有规律,比如 “按数学成绩从高到低排序”“按总分从低到高排序”。
3.4.1 排序的核心语法
SELECT … FROM 表名 [WHERE …] ORDER BY 列1 [ASC/DESC], 列2 [ASC/DESC], …;
语法拆解:
- 列1, 列2, …:指定要排序的列,可以是原始列、表达式、别名(注意,order by就可以使用别名了哦);
- ASC:升序排序(从上到下为从小到大),默认值,可以省略;
- DESC:降序排序(从上到下为从大到小),必须显式指定;
- 多列排序:优先级按书写顺序,先按列 1 排序,列 1 值相同的记录,再按列 2 排序。
3.4.2 经典排序场景实战
我们用 6 个经典场景,把排序的用法讲透,包括单字段排序、多字段排序、表达式排序、别名排序、NULL 值排序
— 创建表结构
CREATE TABLE exam_result (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL COMMENT '同学姓名',
chinese float DEFAULT 0.0 COMMENT '语文成绩',
math float DEFAULT 0.0 COMMENT '数学成绩',
english float DEFAULT 0.0 COMMENT '英语成绩'
);
— 插入测试数据
INSERT INTO exam_result (name, chinese, math, english) VALUES
('唐三藏', 67, 98, 56),
('孙悟空', 87, 78, 77),
('猪悟能', 88, 98, 90),
('曹孟德', 82, 84, 67),
('刘玄德', 55, 85, 45),
('孙权', 70, 73, 78),
('宋公明', 75, 65, 30);

场景 1:单字段升序排序(默认 ASC)
示例:查询学生姓名和数学成绩,按数学成绩升序排序(从小到大):
SELECT name, math FROM exam_result ORDER BY math;
执行后,数学成绩从低到高排列,宋公明(65 分)在最前面,唐三藏和猪悟能(98 分)在最后面。

这里要注意,order by前面不能直接就加where哦,换句话来说where后面是不能直接就加order by的,中间必须要隔个筛选条件,不然就会报错:

那么我们加上筛选条件的话,MySQL就会先根据筛选条件筛选完数据之后,再去进行order by语句进行排序。

场景 2:单字段降序排序(显式 DESC)
示例:查询学生姓名和数学成绩,按数学成绩降序排序(从大到小):
SELECT name, math FROM exam_result ORDER BY math DESC;
执行后,数学成绩从高到低排列,唐三藏和猪悟能(98 分)在最前面,宋公明(65 分)在最后面。

场景 3:多字段排序(先降序,后升序)
示例:查询学生姓名、数学成绩、英语成绩,先按数学降序,再按英语升序排序:
SELECT name, math, english FROM exam_result ORDER BY math DESC, english;
#注意中间要用逗号隔开
执行后,数学成绩高的记录排在前面;数学成绩相同的记录(唐三藏和猪悟能都是 98 分),按英语成绩升序排序 —— 唐三藏的英语成绩 56 分,猪悟能的英语成绩 90 分,所以唐三藏排在猪悟能前面。

场景 4:表达式排序(按总分降序)
示例:查询学生姓名和总分,按总分降序排序:
SELECT name, chinese+math+english 总分 FROM exam_result ORDER BY chinese+math+english DESC;
执行后,总分最高的猪悟能(276 分)排在最前面,总分最低的宋公明(170 分)排在最后面。
场景 5:别名排序(更简洁,推荐使用)
那么我们要知道,因为排序是在筛选之后才进行排序的,所以,order by是可以使用别名的哦!!!
这个主要还是因为MySQL语句的执行顺序,我们后面会专门一篇博客来进行讲解。
示例:查询学生姓名和总分,用别名 “总分” 排序,语法更简洁:
SELECT name, chinese+math+english 总分 FROM exam_result ORDER BY 总分 DESC;
执行结果和场景 4 完全一致,但语法更易读 —— 这就是别名的优势,ORDER BY的执行顺序在SELECT之后,所以可以直接用别名。

场景 6:NULL 值排序(NULL 视为最小值)
示例:查询学生姓名和 QQ 号,按QQ 号升序排序:
SELECT name, qq FROM students ORDER BY qq;
执行后,QQ 号为 NULL 的学生(唐三藏、曹孟德)排在最前面,QQ 号不为 NULL 的学生(孙悟空)排在最后面 —— 因为 MySQL 中,NULL 被视为 “比任何值都小”。
如果按 QQ 号降序排序,NULL 值会排在最后面,语法如下:
SELECT name, qq FROM students ORDER BY qq DESC;
3.4.3 排序的避坑要点
排序虽然简单,但也有 3 个核心注意事项,我们逐一说明:
3.5 LIMIT:分页查询(避免查询全表数据,保护数据库)
当表中数据量很大时(比如 100 万行),一次性查询全表数据会导致数据库卡死
而LIMIT子句的作用,就是 “分页查询数据”,只返回你需要的前 N 条记录,或者从第 M 条开始的 N 条记录,完美解决了 “查询全表数据” 的问题。
而且,一般来说,limit的执行优先级,是最低的,所以我们一般也是把它放在后面的哦。
3.5.1 分页的三种语法(推荐第三种,可读性最强)
MySQL 支持三种LIMIT语法,我们分别讲解,最后推荐最优写法:
SELECT … FROM 表名 LIMIT n;
示例:查询成绩表的前 3 条记录:SELECT * FROM exam_result LIMIT 3;
SELECT … FROM 表名 LIMIT s, n;
注意:这里的s是起始下标,从 0 开始,不是从 1 开始。比如LIMIT 3, 3代表 “从第 4 条记录开始,查询 3 条记录”;
SELECT … FROM 表名 LIMIT n OFFSET s;
这个语法和语法 2 的效果完全一致,但可读性更强 ——LIMIT n代表 “查询 n 条记录”,OFFSET s代表 “跳过前 s 条记录”。

3.5.2 经典分页场景实战(按页查询,覆盖 90% 的业务需求)
实际业务中,最常见的需求是 “按页查询数据”,比如 “每页显示 3 条记录,查询第 1 页、第 2 页、第 3 页”。
我们以成绩表为例,搭配ORDER BY子句(确保分页顺序稳定),完整实现按页查询:
- 分页参数说明:每页条数page_size=3,当前页码page_num,起始下标s=(page_num-1)*page_size;
- 第 1 页:page_num=1,s=(1-1)*3=0,语法如下:
SELECT id, name, math, english, chinese FROM exam_result ORDER BY id LIMIT 3 OFFSET 0;
结果返回 id=1、2、3 的 3 条记录;
- 第 2 页:page_num=2,s=(2-1)*3=3,语法如下:
SELECT id, name, math, english, chinese FROM exam_result ORDER BY id LIMIT 3 OFFSET 3;
结果返回 id=4、5、6 的 3 条记录;
- 第 3 页:page_num=3,s=(3-1)*3=6,语法如下:
SELECT id, name, math, english, chinese FROM exam_result ORDER BY id LIMIT 3 OFFSET 6;
结果返回 id=7 的 1 条记录(因为表中只有 7 条记录),不足 3 条也不会报错。
3.5.3 分页的核心注意事项
分页查询虽然简单,我们逐一说明:
- 统计总条数:SELECT COUNT(*) FROM exam_result;(结果是 7);
- 计算总页数:总页数=CEIL(总条数/每页条数)=CEIL(7/3)=3(CEIL是向上取整函数)。
四、Update(更新数据):修改表里已有的数据(务必慎用!)
更新数据的核心语法是UPDATE,它的作用是 “修改表里已有的数据”。但这个操作务必慎用—— 尤其是不带WHERE子句的UPDATE,会修改全表数据,造成灾难性后果。
我们可以把UPDATE比作 “修改 Excel 表格的单元格内容”:
- UPDATE 表名:指定你要修改哪张表;
- SET 列=值:指定你要修改哪些列,改成什么值;
- WHERE 条件:指定你要修改哪些行,几乎必加,否则修改全表。
4.1 更新的核心语法结构
UPDATE 表名
SET 列1 = 表达式1, 列2 = 表达式2, …
[WHERE/order by 筛选条件]
[ORDER BY 排序条件]
[LIMIT 限制数量];
语法拆解:
- SET:必填项,指定要修改的列和新值。新值可以是固定值、表达式(比如math = math + 30)、函数(比如name = UPPER(name));
- WHERE:可选项,但实战中几乎必加,筛选要修改的行。不加WHERE会修改全表数据,生产环境中 99% 的误操作都是因为这个,注意,也可以只使用order by哦。
where+set就是说明,我们要修改哪一行(符合where条件的那一行)的哪个字段(set后面所跟着的字段)
- ORDER BY:可选项,对要修改的行排序,搭配LIMIT使用,比如 “修改总分倒数前三的学生”;
- LIMIT:可选项,限制修改的行数,避免一次修改太多数据。
注意不用加from哦。
4.2 经典更新场景实战
场景 1:修改单条数据(指定值更新,最基础)
示例:把 “孙悟空” 的数学成绩从 78 分改成 80 分。
操作步骤(安全第一,三步法)
更新数据的安全三步法:先查、再更、再验证,确保万无一失。
SELECT name, math FROM exam_result WHERE name = '孙悟空';
执行后,确认孙悟空的数学成绩是 78 分;
UPDATE exam_result SET math = 80 WHERE name = '孙悟空';
执行后,数据库返回提示:Query OK, 1 row affected (0.04 sec),代表修改了 1 行数据;
SELECT name, math FROM exam_result WHERE name = '孙悟空';
执行后,确认孙悟空的数学成绩已经变成 80 分。

避坑要点
- 必须加WHERE条件,且条件要精准:比如用name='孙悟空',而不是math=78,因为可能有多个学生的数学成绩是 78 分;
- 先查询再更新:这是最安全的操作习惯,尤其是在生产环境中,永远不要直接执行UPDATE语句。
场景 2:修改多条数据(多列同时更新,实战常用)
示例:把 “曹孟德” 的数学成绩改成 60 分,语文成绩改成 70 分。
语法 & 步骤
SELECT name, math, chinese FROM exam_result WHERE name = '曹孟德';
UPDATE exam_result SET math = 60, chinese = 70 WHERE name = '曹孟德';
SELECT name, math, chinese FROM exam_result WHERE name = '曹孟德';
核心优势
- 一次修改多列数据,效率高:比执行两次UPDATE语句快一倍;
- 语法简洁:多个列之间用逗号分隔,清晰明了。
场景 3:基于原值更新(表达式更新,超实用)
示例:给 “总分倒数前三的学生” 的数学成绩加 30 分。
语法 & 步骤
这种场景需要搭配ORDER BY和LIMIT使用,核心是 “先排序,再限制行数,最后更新”。
SELECT name, math, chinese+math+english 总分 FROM exam_result ORDER BY 总分 LIMIT 3;
结果是宋公明、刘玄德、曹孟德;
UPDATE exam_result SET math = math + 30 ORDER BY chinese+math+english LIMIT 3;
注意:MySQL 不支持math += 30这种简写语法,必须写math = math + 30;
SELECT name, math FROM exam_result WHERE name IN ('宋公明', '刘玄德', '曹孟德');
避坑要点
- 表达式更新要注意数据类型:比如math是FLOAT类型,加 30 后不会溢出;如果是INT类型,要确保结果不超过列的最大值;
- ORDER BY和LIMIT的顺序不能反:必须先ORDER BY排序,再LIMIT限制行数,否则会修改错误的行。
场景 4:修改全表数据(慎用!务必先备份)
示例:把所有学生的语文成绩改成原来的 2 倍。
语法 & 步骤
CREATE TABLE exam_result_backup LIKE exam_result;
INSERT INTO exam_result_backup SELECT * FROM exam_result;
UPDATE exam_result SET chinese = chinese * 2;
执行后,数据库返回提示:Query OK, 7 rows affected (0.05 sec),代表修改了 7 行数据;
SELECT name, chinese FROM exam_result;
避坑警告
- 生产环境中,修改全表数据前必须备份:一旦修改错误,只能通过备份恢复数据;
- 尽量分批次修改:如果表中有 100 万行数据,不要一次修改全表,分 10 次,每次修改 10 万行,避免锁表;
- 在业务低峰期执行:全表更新会锁表,导致其他用户无法读写数据,最好在凌晨等低峰期执行。
4.3 更新的核心避坑指南
更新数据是高危操作:
START TRANSACTION; — 开启事务
UPDATE exam_result SET math = 80 WHERE name = '孙悟空'; — 执行更新
SELECT name, math FROM exam_result WHERE name = '孙悟空'; — 验证结果
COMMIT; — 确认正确,提交事务
— ROLLBACK; — 如果错误,回滚事务
五、Delete(删除数据):移除表里不需要的行
删除数据的核心语法是DELETE,它的作用是 “移除表里不需要的行数据”。这个操作比更新更危险—— 删除的数据很难恢复,尤其是不带WHERE的DELETE,会删除全表数据,造成无法挽回的损失。
我们可以把DELETE比作 “删除 Excel 表格的整行数据”:
- DELETE FROM 表名:指定你要删除哪张表的数据;
- WHERE 条件:指定你要删除哪些行,几乎必加,否则删除全表;
- ORDER BY/LIMIT:指定删除的顺序和行数,避免一次删除太多数据。
5.1 删除的核心语法结构
DELETE FROM 表名
[WHERE 筛选条件]
[ORDER BY 排序条件]
[LIMIT 限制数量];
语法拆解:
- DELETE FROM:固定关键字组合,指定要删除数据的表;
- WHERE:可选项,但实战中几乎必加,筛选要删除的行。不加WHERE会删除全表数据;
- ORDER BY:可选项,对要删除的行排序,搭配LIMIT使用;
- LIMIT:可选项,限制删除的行数,避免一次删除太多数据。
5.2 经典删除场景实战
场景 1:删除单条数据(最基础,安全第一)
示例:删除 “孙悟空” 的考试成绩记录。
操作步骤(安全三步法)
删除数据的安全三步法:先查、再删、再验证,和更新数据一样,安全永远是第一位的。
SELECT * FROM exam_result WHERE name = '孙悟空';
执行后,确认要删除的记录是 id=2 的行;
DELETE FROM exam_result WHERE name = '孙悟空';
执行后,数据库返回提示:Query OK, 1 row affected (0.04 sec),代表删除了 1 行数据;
SELECT * FROM exam_result WHERE name = '孙悟空';
执行后,返回空结果,代表记录已成功删除。
避坑要点
- 必须加WHERE条件,且条件要精准:比如用name='孙悟空',或更精准的id=2;
- 不要用DELETE * FROM 表名:这种语法是错误的,DELETE后面不需要加*,直接写DELETE FROM 表名即可。
场景 2:删除全表数据(DELETE,慎用!可回滚)
示例:删除测试表 for_delete 的所有数据。
操作步骤(务必先备份)
第一步:创建测试表并插入数据先准备一张测试表,避免操作生产数据:
CREATE TABLE for_delete (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20)
);
INSERT INTO for_delete (name) VALUES ('A'), ('B'), ('C');
执行后,表中有 3 条数据,id 分别为 1、2、3。
第二步:备份数据(关键!)执行删除全表操作前,一定要备份:
CREATE TABLE for_delete_backup LIKE for_delete;
INSERT INTO for_delete_backup SELECT * FROM for_delete;
第三步:执行全表删除不带 WHERE 子句的 DELETE 会删除全表数据:
DELETE FROM for_delete;
执行后,数据库返回提示:Query OK, 3 rows affected (0.03 sec),代表删除了 3 行数据。
第四步:验证结果查询表数据,确认全表已空:
SELECT * FROM for_delete;
执行后返回空结果,说明数据已删除。
第五步:测试自增列特性往空表中插入一条新数据:
INSERT INTO for_delete (name) VALUES ('D');
查询结果会发现,新数据的 id 是 4,不是 1—— 这是 DELETE 删除全表的关键特性:自增列不会重置,会从删除前的最大值 + 1 继续递增。
核心特点(DELETE 删除全表)
- 属于 DML 操作:DELETE 是数据操作语言,执行后可以通过事务回滚(前提是未提交事务)。
- 可以筛选和限制行数:如果加上 WHERE 和 LIMIT,可以只删除部分数据,比如 DELETE FROM for_delete WHERE id < 3 LIMIT 2;。
- 自增列不重置:如上述示例,删除后插入新数据,id 从 4 开始,因为自增计数器的数值没有被清空。
- 效率较低:DELETE 是逐行删除数据,会记录每一行的删除日志,数据量越大,删除速度越慢。
场景 3:截断表(TRUNCATE,彻底清空,无法回滚)
如果需要彻底清空表并重置自增列,可以使用 TRUNCATE 语句。它和 DELETE 删除全表的逻辑完全不同,更高效,但风险也更高。
操作步骤
第一步:创建测试表并插入数据
CREATE TABLE for_truncate (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20)
);
INSERT INTO for_truncate (name) VALUES ('A'), ('B'), ('C');
表中有 3 条数据,id 为 1、2、3。
第二步:执行截断表操作
TRUNCATE TABLE for_truncate;
执行后,数据库返回提示:Query OK, 0 rows affected (0.01 sec)—— 这里的 0 rows affected 是因为 TRUNCATE 不逐行处理数据,直接清空表。
第三步:验证结果查询表数据,确认全表已空:
SELECT * FROM for_truncate;
第四步:测试自增列特性插入一条新数据:
INSERT INTO for_truncate (name) VALUES ('D');
查询结果会发现,新数据的 id 是 1—— 这是 TRUNCATE 的关键特性:重置自增列,自增计数器会被重置为初始值(通常是 1)。
DELETE vs TRUNCATE 核心区别(必须牢记)
| 操作类型 | DML(数据操作语言) | DDL(数据定义语言) |
| 能否回滚 | 可以(事务未提交时执行 ROLLBACK) | 不可以(直接修改表结构,无法回滚) |
| 自增列是否重置 | 否(从最大值 + 1 继续递增) | 是(重置为初始值 1) |
| 能否筛选 / 限制行数 | 能(加 WHERE/LIMIT) | 不能(只能清空全表) |
| 执行效率 | 低(逐行删除,记录日志) | 高(不处理数据,直接清空表) |
| 适用场景 | 需要删除部分数据,或需回滚的场景 | 彻底清空测试表,且无需保留自增列的场景 |
避坑警告
- TRUNCATE 是高危操作,执行后数据无法回滚,生产环境中务必确认表名正确,且数据已备份。
- TRUNCATE 会清空表的所有数据,但保留表结构,表的列、索引、约束等不会被删除。
- 如果表有外键约束,TRUNCATE 会报错,需要先删除外键约束,再执行截断操作。
5.3 Delete 的核心避坑指南
删除数据是不可逆操作(尤其是 TRUNCATE),一旦误删,损失可能无法挽回:
START TRANSACTION; — 开启事务
DELETE FROM exam_result WHERE name = '孙悟空'; — 执行删除
SELECT * FROM exam_result WHERE name = '孙悟空'; — 验证是否删除正确
COMMIT; — 确认无误,提交事务
— ROLLBACK; — 如果误删,立即回滚
DELETE FROM exam_result WHERE id < 10000 LIMIT 1000; — 每次删1000行
循环执行,直到删除完毕。
- 主表 course(课程表),子表 student(学生表),外键 courseid 关联 course.id,且设置了 ON DELETE CASCADE。
- 删除 course 表中 id=1 的课程,student 表中所有 courseid=1 的学生数据会被自动删除。因此,执行删除前,务必确认外键的级联规则。
六、进阶操作:插入查询结果 & 聚合函数 & 分组查询
掌握了基础的 CRUD 之后,我们需要学习三个进阶操作,这些操作在实际开发中使用频率极高,是实现 “数据统计、数据去重、数据迁移” 的核心。
6.1 插入查询结果:把一个表的查询结果插入到另一个表
核心语法:
INSERT INTO 目标表 [(列1, 列2, …)] SELECT 列1, 列2, … FROM 源表 [WHERE 筛选条件];
这个语法的作用是:将源表的查询结果,直接插入到目标表中,无需手动逐条插入,效率极高。
经典场景:删除表中的重复记录(只保留一份)
在实际开发中,表中可能会出现重复数据(比如批量导入时重复上传),我们需要删除重复记录,只保留一份。步骤如下:
第一步:创建原数据表并插入重复数据
CREATE TABLE duplicate_table (id INT, name VARCHAR(20));
INSERT INTO duplicate_table VALUES
(100, 'aaa'), (100, 'aaa'), (200, 'bbb'), (200, 'bbb'), (200, 'bbb'), (300, 'ccc');

表中有 6 条数据,其中 (100, 'aaa') 重复 2 次,(200, 'bbb') 重复 3 次。
第二步:创建一张和原表结构相同的空表使用 LIKE 关键字,快速创建结构一致的空表:
CREATE TABLE no_duplicate_table LIKE duplicate_table;

可以看到,两个表的创建操作是一样的,这就是一个大大提高效率的方法。
第三步:将原表去重后的结果插入新表使用 SELECT DISTINCT 去重,再插入新表:
INSERT INTO no_duplicate_table SELECT DISTINCT * FROM duplicate_table;

第四步:重命名表,实现原子去重通过重命名表,将新表替换原表,这个操作是原子的(瞬间完成,不会导致数据丢失):
RENAME TABLE duplicate_table TO old_duplicate_table, no_duplicate_table TO duplicate_table;

第五步:验证结果查询新表,确认重复数据已被删除:
SELECT * FROM duplicate_table;

结果会显示 3 条数据:(100, 'aaa'), (200, 'bbb'), (300, 'ccc'),完美去重。
核心优势
- 效率极高:直接通过数据库内部操作完成数据迁移和去重,比 “查出来再逐条插入” 快 10 倍以上。
- 原子操作:重命名表的过程是瞬间完成的,不会出现 “数据缺失” 的情况。
- 适用场景广:除了去重,还可以用于 “数据备份”“数据分表”“统计结果存储” 等场景。
6.2 聚合函数:对数据进行统计计算
聚合函数是对一组数据进行计算,返回单个值的函数,是实现 “数据统计” 的核心。
| COUNT([DISTINCT] 列/表达式) | 统计行数(数据的数量),也就是某一个字段有几行 | 1. COUNT(*):统计所有行,包含 NULL 值;2. COUNT(列):统计该列非 NULL 的行数;3. COUNT(DISTINCT 列):统计该列非 NULL 且不重复的行数。 |
| SUM([DISTINCT] 列) | 计算该列的总和 | 1. 只对数字类型的列有效;2. 忽略 NULL 值;3. SUM(DISTINCT 列):计算去重后的总和。 |
| AVG([DISTINCT] 列) | 计算该列的平均值 | 1. 只对数字类型的列有效;2. 公式:AVG(列) = SUM(列) / COUNT(列);3. 忽略 NULL 值。 |
| MAX(列) | 计算该列的最大值 | 1. 对数字、字符串、日期类型都有效;2. 忽略 NULL 值;3. 字符串按 “字符编码” 比较大小。 |
| MIN(列) | 计算该列的最小值 | 同 MAX(列) 的注意点。 |
注意这些函数的使用都是加在select后面的哦,而不是where后面,大家详细的要看下面的示例进行理解哦,然后自己动手敲敲。
经典聚合场景实战
以 exam_result 成绩表和 students 学生表为例:
统计班级总人数用 COUNT(*) 统计,包含 NULL 值:
SELECT COUNT(*) FROM students;

结果是 6,代表班级有 6 个学生。
统计有 QQ 号的学生数用 COUNT(qq) 统计,忽略 NULL 值:
SELECT COUNT(qq) FROM students;

结果是 2,代表只有 2 个学生填了 QQ 号。
统计数学成绩的去重数量用 COUNT(DISTINCT math) 统计:
SELECT COUNT(DISTINCT math) FROM exam_result;

结果是 7,代表数学成绩有 7 种不同的分数。
统计数学成绩的总分用 SUM(math) 计算:
SELECT SUM(math) FROM exam_result;

结果是 673,代表所有学生的数学成绩总和是 673 分。
统计所有学生的总分平均分用 AVG(表达式) 计算:
SELECT AVG(chinese + math + english) 平均总分 FROM exam_result;

结果是 297.5,代表班级的平均总分是 234分。
查询英语成绩的最高分和最低分用 MAX(english) 和 MIN(english) 计算:
SELECT MAX(english) 英语最高分, MIN(english) 英语最低分 FROM exam_result;

结果是 90 和 30,代表英语最高分是 90 分,最低分是 30 分。
6.3 GROUP BY:按 “类别” 分组统计数据
先明确:GROUP BY 相关 SQL 的执行顺序
这是理解 GROUP BY 逻辑的核心,MySQL 中包含 GROUP BY 的 SQL 语句,执行顺序固定为:
简单总结执行逻辑:先找表 → 筛原始数据 → 分组 → 算统计值 → 筛统计结果 → 选展示列 → 排序 → 限制行数。记住这个顺序,就能理解为什么 WHERE 不能用聚合函数(因为聚合函数在 GROUP BY 之后才计算),而 HAVING 可以(HAVING 在聚合函数计算后执行)。
这里我和大家说一下,无论如何,必须要把这些顺理解透,它是我们掌握group by的重中之重。
理解:GROUP BY 到底是干什么的?
生活类比:老师的 “分堆统计”
想象你是班主任,手里拿着全班的考试成绩表,现在需要做几个统计:
- 统计每个学生 3 科的总分和平均分 → 按「学生姓名」把成绩分成 7 堆(每个学生一堆),每堆算一次总分 / 平均分;
- 统计每门科目的平均分和最高分 → 按「科目」把成绩分成 3 堆(语文 / 数学 / 英语各一堆),每堆算一次平均分 / 最高分;
- 统计每科及格和不及格的人数 → 先按「科目」分 3 堆,每堆再按「是否及格」分成 2 小堆,每小堆算人数。
这个 “分堆→统计” 的过程,就是 GROUP BY 的核心逻辑:把原始数据按指定规则分成若干组,然后对每组单独进行聚合统计。
数据源:我们的考试成绩表
先明确我们要操作的表结构:
| 1 | 唐三藏 | 67 | 128 | 56 |
| 2 | 孙悟空 | 87 | 80 | 77 |
| 3 | 猪悟能 | 88 | 98 | 90 |
| 4 | 曹孟德 | 82 | 84 | 67 |
| 5 | 刘玄德 | 55 | 115 | 45 |
| 6 | 孙权 | 70 | 73 | 78 |
| 7 | 宋公明 | 75 | 95 | 30 |
核心语法拆解:每个部分的作用
GROUP BY 的完整语法如下,我们逐部分解释:
SELECT 分组列1, 分组列2, 聚合函数1, 聚合函数2
FROM exam_result
[WHERE 分组前筛选条件]
GROUP BY 分组列1, 分组列2
[HAVING 分组后筛选条件];
1. SELECT 子句:只能包含两种内容
规则:SELECT 里的列,必须是 GROUP BY 后面的「分组列」,或者是「聚合函数」(比如 SUM/AVG/MAX 等)。
为什么?
因为分组后,每组会被压缩成一行统计结果,不再包含组内的单个原始数据。比如按「姓名」分组后,每组只有一个学生的统计值(总分 / 平均分),不能直接显示该学生某一次的数学分数(因为组内只有一个统计行,没有原始的数学分数行)。
正确示例(按学生分组,统计总分):
SELECT name, SUM(chinese + math + english) AS 总分
FROM exam_result
GROUP BY name;
- name 是分组列 → 每个学生一行;
- SUM (…) 是聚合函数 → 每个学生的 3 科总分。

错误示例(包含非分组列、非聚合函数):
SELECT name, math, SUM(chinese + math + english) AS 总分
FROM exam_result
GROUP BY name;
- math 既不是分组列,也不是聚合函数 → 分组后每组只有一行,无法确定显示哪个 math 值(每个学生只有一个 math 值,这里看似没问题,但如果是按科目分组,就会出现多个 math 值,导致逻辑错误)。
2. FROM 子句:指定数据源
这里就是我们的考试成绩表 exam_result,用于加载原始数据,是 SQL 执行的第一步。
3. WHERE 子句:分组前的筛选(先筛再分)
作用:在分组之前,先对原始数据进行筛选,只保留符合条件的数据进入分组。
限制:WHERE 里不能使用聚合函数(因为聚合函数是 GROUP BY 之后才计算的,分组前没有统计值)。
示例:只统计「语文≥60 分」的学生的总分
SELECT name, SUM(chinese + math + english) AS 总分
FROM exam_result
WHERE chinese >= 60 — 分组前筛:排除语文<60的学生(刘玄德被排除)
GROUP BY name;
- 原始表中「刘玄德」的语文是 55 分,不符合 WHERE 条件,所以不会进入分组,最终结果里没有他的记录。
4. GROUP BY 子句:指定分组规则
作用:按指定的列(或表达式)把数据分成若干组,相同值的记录会被分到同一组。
多列分组:可以指定多个分组列,规则是「先按第一列分,再按第二列细分」,比如 GROUP BY 科目,及格状态 会先按科目分成 3 组,每组再按及格状态分成 2 小组。
示例 1:单列分组(按学生姓名)
SELECT name, SUM(chinese + math + english) AS 总分
FROM exam_result
GROUP BY name;
- 按 name 分组,每个学生一组,每组计算一次总分。
示例 2:多列分组(按科目 + 及格状态)
— 先把科目转成列→行(用 UNION ALL 做列转行)
SELECT '语文' AS 科目, chinese AS 分数 FROM exam_result
UNION ALL
SELECT '数学' AS 科目, math AS 分数 FROM exam_result
UNION ALL
SELECT '英语' AS 科目, english AS 分数 FROM exam_result
— 再对转换后的结果分组统计
SELECT
科目,
CASE WHEN 分数 >= 60 THEN '及格' ELSE '不及格' END AS 及格状态,
COUNT(*) AS 人数
FROM (
SELECT '语文' AS 科目, chinese AS 分数 FROM exam_result
UNION ALL
SELECT '数学' AS 科目, math AS 分数 FROM exam_result
UNION ALL
SELECT '英语' AS 科目, english AS 分数 FROM exam_result
) AS subject_scores
GROUP BY 科目, 及格状态;
- 先把原始表的 3 科分数转成「科目 + 分数」的格式(列转行),再按「科目」和「及格状态」分组,统计每科及格 / 不及格的人数。
5. HAVING 子句:分组后的筛选(先分再筛)
作用:在分组之后,对聚合统计的结果进行筛选,只保留符合条件的分组。
特点:HAVING 里可以使用聚合函数(因为分组后已经有了统计值)。
示例:统计「总分≥230 分」的学生
SELECT name, SUM(chinese + math + english) AS 总分
FROM exam_result
GROUP BY name
HAVING 总分 >= 230; — 分组后筛:只保留总分≥230的学生
- 分组后,每个学生的总分已经计算完成,HAVING 会筛选出总分≥230 的分组,最终结果里只有唐三藏、孙悟空、猪悟能、曹孟德 4 个学生。
6. 聚合函数:分组统计的 “工具包”
聚合函数是对每组数据进行统计的工具,常用的有:
| SUM() | 求和 → 计算学生 3 科总分 | SUM(chinese + math + english) |
| AVG() | 求平均 → 计算学生 3 科平均分 | AVG(chinese + math + english) |
| MAX() | 求最大值 → 计算学生某科最高分 | MAX(math) |
| MIN() | 求最小值 → 计算学生某科最低分 | MIN(english) |
| COUNT() | 计数 → 统计每科及格人数 | COUNT(*) |
示例:按学生分组,统计总分、平均分、最高单科分
SELECT
name,
SUM(chinese + math + english) AS 总分,
ROUND(AVG(chinese + math + english), 1) AS 平均分,
GREATEST(chinese, math, english) AS 最高单科分
FROM exam_result
GROUP BY name;
注:MySQL 中没有 MAX 多列取值的函数,需用 GREATEST () 替代,作用是取多个列中的最大值。
完整实战:从需求到 SQL 的完整流程
需求:统计「语文≥60 分」的学生中,「总分≥230 分」的学生的姓名、总分和平均分
步骤拆解:
最终 SQL:
SELECT
name,
SUM(chinese + math + english) AS 总分,
ROUND(AVG(chinese + math + english), 1) AS 平均分
FROM exam_result
WHERE chinese >= 60
GROUP BY name
HAVING 总分 >= 230;

执行结果:
| 唐三藏 | 251 | 83.7 |
| 孙悟空 | 244 | 81.3 |
| 猪悟能 | 276 | 92.0 |
| 曹孟德 | 233 | 77.7 |
结合执行顺序理解这个 SQL:
常见坑点避坑指南
坑点 1:SELECT 列包含非分组列 / 非聚合函数
错误:SELECT name, math, SUM (…) FROM exam_result GROUP BY name
;正确:要么把 math 改成聚合函数(比如 MAX (math)),要么把 math 加入 GROUP BY(但按 name, math 分组不符合业务需求)。
坑点 2:WHERE 里使用聚合函数
错误:SELECT name, SUM (…) FROM exam_result WHERE SUM (…) >= 230 GROUP BY name;
正确:把聚合函数的筛选放到 HAVING 里:HAVING SUM (…) >= 230。
坑点 3:多列分组时,GROUP BY 列和 SELECT 列不对应
错误:SELECT 科目,及格状态,COUNT () FROM … GROUP BY 科目;
正确:SELECT 科目,及格状态,COUNT () FROM … GROUP BY 科目,及格状态;(SELECT 里的列必须和 GROUP BY 列完全对应)。
坑点 4:COUNT (*) 和 COUNT (列名) 的区别
- COUNT (*):统计组内的总记录数(包括 NULL 值);
- COUNT (列名):统计组内该列不为 NULL 的记录数。比如统计英语成绩不为 NULL 的人数:COUNT (english);统计总人数:COUNT (*)。
坑点 5:混淆 MAX 单列和多列的用法
错误:MAX (chinese, math, english);
正确:GREATEST (chinese, math, english)(MySQL 中取多列最大值用 GREATEST,最小值用 LEAST)。
核心总结
结语:以 CRUD 为舟,驶向 MySQL 的星辰大海
亲爱的同学,当你看到这里时,相信你已经跟着这份教程,走完了 MySQL 数据操作从基础到进阶的完整旅程 —— 从最初对 CRUD 四个字母的陌生,到能熟练写出插入多行数据的语句,能精准用 WHERE 筛选出想要的数据,能小心翼翼地执行更新和删除操作,甚至能通过 GROUP BY 完成分组统计,这一路的收获,值得你为自己鼓掌。
我们的学习始终遵循 “由外到内” 的逻辑:从数据库到表,再到表中最核心的数据,而 CRUD 正是我们与数据对话的 “通用语言”。回头看,这趟学习之旅里没有晦涩难懂的 “空中楼阁”,每一个知识点都锚定着实际的业务场景:新增数据时,我们知道了批量插入比单行插入更高效,知道了用 INSERT … ON DUPLICATE KEY UPDATE 解决主键冲突;查询数据时,我们摆脱了只会用 SELECT * 的新手误区,懂得了指定列查询的性能优势,能用 WHERE 做精准筛选,用 ORDER BY 让数据有序,用 LIMIT 保护数据库不被全表查询拖垮;更新和删除数据时,我们牢牢记住 “先查再操作、必加 WHERE 条件、开启事务验证” 的安全准则,避开了 “修改全表、误删数据” 的致命坑;进阶学习中,我们学会了用插入查询结果去重,用聚合函数做数据统计,用 GROUP BY 完成 “分堆计算”,理解了 FROM→WHERE→GROUP BY→HAVING 的执行顺序 —— 这些不是孤立的语法,而是解决实际问题的 “工具箱”,是你未来应对业务需求的核心底气。
我知道,学习的过程里你一定有过困惑:比如第一次忘记给 NULL 值赋值导致插入报错,第一次用 WHERE 筛选 NULL 值时用了 = 而不是 IS NULL,第一次写 GROUP BY 时因为 SELECT 里加了非分组列而报错,甚至可能在测试时不小心执行了不带 WHERE 的 UPDATE,看着全表数据被修改时慌了神。但请你记住:踩坑不是学习的 “失误”,而是掌握 MySQL 的 “必经之路”。这些错误恰恰让你理解了 MySQL 的底层逻辑 —— 它不是一套死板的语法规则,而是有自己执行顺序、数据约束、安全机制的 “数据管家”:它要求值的类型与字段匹配,是为了保证数据的准确性;它要求更新删除必加 WHERE,是为了防止数据被误操作;它的聚合函数要在 GROUP BY 后执行,是为了保证统计结果的合理性。理解这些 “为什么”,远比死记 “怎么写” 更重要,因为当你懂了底层逻辑,哪怕忘记了某个语法细节,也能凭借逻辑推导出正确的写法。
MySQL 从来不是 “看会” 的,而是 “练会” 的。这份教程里的每一个示例、每一个场景,都需要你亲手敲一遍代码:创建学生表、插入测试数据、执行查询语句、修改数据后验证结果、删除数据前先备份…… 只有当你的手指敲过这些语句,当你亲眼看到 “Query OK, 1 row affected” 的提示,当你排查过 “Column count doesn't match value count” 的报错,这些知识才会真正变成你的 “肌肉记忆”。你可以试着给自己布置一些模拟业务场景:比如 “统计一个班级各科的平均分和及格率”“清理一张表中的重复用户数据”“给不同分数段的学生更新等级标签”,把学到的 CRUD 和聚合、分组知识结合起来,你会发现,那些看似零散的语法,会在解决问题的过程中形成完整的体系。
当然,CRUD 只是 MySQL 学习的 “第一站”。未来你还会接触到多表连接查询、索引优化、事务与锁、存储过程等更深的知识点,但请相信,今天你打下的 CRUD 基础,是解锁所有进阶内容的 “钥匙”:多表查询本质上是多表的 “联合查询 + 筛选”,索引优化是为了让 SELECT 查询更快,事务是为了保证更新 / 删除操作的原子性 —— 所有高阶知识点,都是在 CRUD 的基础上解决 “更复杂、更高效、更安全” 的问题。所以不必急于求成,把今天的知识点吃透,把每一个避坑要点记牢,比如 “永远不要在生产环境执行不带 WHERE 的 UPDATE/DELETE”“分页查询必加 ORDER BY 保证顺序稳定”“GROUP BY 的 SELECT 列只能是分组列或聚合函数”,这些看似琐碎的规则,会成为你未来工作中 “保护数据安全” 的第一道防线。
学习编程从来不是 “一蹴而就” 的事,就像我们操作数据时要 “一步一步验证”,学习的过程也需要 “慢慢来,比较快”。可能你现在还不能立刻把这些知识应用到实际项目中,可能你还会偶尔混淆 HAVING 和 WHERE 的用法,这都没关系。重要的是,你已经推开了 MySQL 数据操作的大门,理解了 “数据是数据库的核心,而 CRUD 是与数据交互的核心” 这个本质。未来的日子里,当你遇到问题时,不妨回头看看这份教程,看看我们用 Excel 类比 CRUD 的通俗解释,看看那些标注了 “避坑要点” 的细节,看看分组查询的执行顺序 —— 这些内容会像 “指南针” 一样,帮你找回思路。
最后,想对你说:数据库是 “数据的仓库”,而你学会的 CRUD,就是管理这个仓库的 “基本功”。无论是未来做后端开发、数据分析,还是测试运维,这些操作都会是你日常工作中最频繁接触的内容。请珍惜这份 “从 0 到 1” 的成长,也请保持对 “为什么” 的好奇 —— 为什么聚合函数不能用在 WHERE 里?为什么 REPLACE 和 INSERT … ON DUPLICATE KEY UPDATE 处理冲突的逻辑不同?为什么 NULL 值参与表达式结果还是 NULL?当你把这些 “为什么” 都弄明白,你就不再是 “只会写语法的新手”,而是 “懂逻辑的开发者”。
愿你以 CRUD 为舟,在 MySQL 的星辰大海里稳步前行,每一次敲下的 SQL 语句,都精准、安全、高效;每一次解决的数据问题,都让你离 “优秀的开发者” 更近一步。别怕犯错,别怕慢,只要你坚持练习、持续思考,终会把这些基础的操作内化成能力,用数据解决真正的业务问题。加油,未来的你,一定会感谢今天认真学完 CRUD 的自己!
