把MySQL的教程看到第三章的时候,我才真正有了"自己在操作数据库"的感觉。前两章基本在讲概念和安装,什么关系型数据库、什么SQL语言分类,看完脑子里装了一堆名词,真到键盘前还是不知道怎么下手。第三章不一样,这一章开始真正碰命令行,建库、建表、往表里插数据,再把这些数据查出来——整个流程一次走完,才算是把MySQL从"听说过"变成了"上手用过"。
这篇笔记记录的是我3月25日复习完第三章之后的整理。里面会包含建库建表的完整流程、增删改查的语法细节,还有我自己在练习时踩过的一些坑。如果你也是学到第三章的初学者,或者刚装好MySQL准备动手敲命令,这篇笔记应该能帮你少走不少弯路。安装环境无论Windows、Linux还是Docker都无所谓,SQL语法本身不区分平台,只要能进入mysql命令行就行。
1. 建库建表:第三章第一节,先把数据的地基打好
第三章一开始没有直接讲怎么查数据,而是先教怎么把库和表建出来。这一节语法很简单,但很多后面的坑其实从这一节就埋下了。
1.1 数据库的创建:字符集从第一步就要选对
建库的SQL简单到让人想跳过:
sql复制CREATE DATABASE student_db;
但如果你真的只用这一句,后面大概率会遇到乱码问题。原因是MySQL默认字符集在不同版本里不一样,5.7默认是latin1,8.0默认是utf8mb4。如果你在5.7上直接建库不指定字符集,插入中文时就会看到满屏的"???"。
建议建库时把字符集和排序规则一起指定:
sql复制CREATE DATABASE student_db
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
这里解释一下为什么是utf8mb4而不是utf8。MySQL的utf8其实是utf8mb3,它最多只能存3个字节的字符,像emoji这种4字节字符根本存不进去。utf8mb4兼容所有Unicode字符,是后面所有版本的主流选择。排序规则我选的是utf8mb4_unicode_ci,它对字符的排序更接近Unicode标准,而且_ci结尾代表大小写不敏感(case insensitive)。
当时我一开始偷懒没指定,后来往表里插入一条包含emoji的备注数据,直接报错"Incorrect string value",查了很久才发现问题出在建库那一步。这个教训让我养成了一个习惯:以后所有建库、建表语句,字符集永远写全。
判断当前库的字符集可以用这个命令:
sql复制SHOW CREATE DATABASE student_db;
它会返回完整的建库语句,里面直接带着CHARSET的设定。如果已经建好了库想补救,可以用ALTER:
sql复制ALTER DATABASE student_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
但要注意,ALTER只改了库的默认字符集,已经建好的表还需要单独改。所以最好是在第一步就指定好,后面不要回头改。
1.2 表结构设计:字段类型决定数据的"上限"
建表的SQL长这样:
sql复制CREATE TABLE student (
id INT NOT NULL AUTO_INCREMENT,
student_no VARCHAR(20) NOT NULL,
name VARCHAR(50) NOT NULL,
gender TINYINT DEFAULT 0,
birthday DATE,
score DECIMAL(5, 2) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_student_no (student_no)
) DEFAULT CHARSET=utf8mb4;
字段类型看起来就是几个英文单词,但选错的话,后面跑数据时会非常难受。我当时整理了一个对照表,觉得对初学者来说特别有用:
| 类型 | 存储空间 | 取值范围或说明 |
|---|---|---|
| TINYINT | 1字节 | -128~127,或0~255(无符号),适合存性别、状态这类小整数 |
| INT | 4字节 | -2147483648~2147483647,最常用的整数类型 |
| BIGINT | 8字节 | 很大的整数,一般数据不会超过INT范围,但某些统计类字段可以考虑 |
| VARCHAR(20) | 按字符存储 | 20是字符数上限,不是字节数,适合姓名、编号等变长文本 |
| CHAR(20) | 固定20字符 | 适合长度固定不变的场景,比如身份证号 |
| DECIMAL(10,2) | 变长 | 精确的小数,适合存钱,总位数10位,小数2位 |
| DATE | 3字节 | 只存日期,比如'2026-03-25' |
| DATETIME | 8字节 | 存日期和时间,比如'2026-03-25 10:30:00' |
关于热搜里那个"mysql中int+5",正好在这块能一起说清楚。INT是整数类型,它参与算术运算时,MySQL会按整数规则处理。比如SELECT 5 + 5结果是10,这是整数;但SELECT 5 / 2结果是2.5000,因为除法在MySQL里默认会转成DECIMAL类型来保证精度。所以别以为INT列加5就一定是整数,要看具体是什么运算。
我当时建表时踩了一个小坑:把score字段设成了FLOAT。看起来没问题,但往里面存89.9,再查出来变成了89.89999999999999。原因是FLOAT是浮点数,用二进制表示十进制小数会有精度损失。后来把所有涉及金额、分数的字段都改成了DECIMAL,才算干净。给初学者的建议是:小数就用DECIMAL,别用FLOAT和DOUBLE。
还有一个容易搞混的点:VARCHAR(20)的20到底代表什么?它代表20个字符,不是20个字节。一个汉字占一个字符,一个英文也占一个字符。所以VARCHAR(20)可以存20个汉字,也可以存20个英文。这一点和CHAR_LENGTH、LENGTH的区别是连在一起的——字符数看CHAR_LENGTH,字节数看LENGTH,后面学函数时还会再碰面。
1.3 约束是数据库的"规则感",不是摆设
建表语句里有几个约束,刚开始我觉得它们多余,后来才发现是保护数据的核心机制。
主键约束(PRIMARY KEY)保证每行记录都能被唯一标识。主键不能为空,也不能重复。这个设计背后其实是关系型数据库的根基——只要你有主键,你就可以精确地UPDATE和DELETE某一行,而不是靠一堆模糊条件去碰运气。
自增约束(AUTO_INCREMENT)通常配合主键使用,让数据库自动生成递增ID。很多初学者会问:为什么业务上已经有一个"学号"字段了,还要额外建一个id?因为学号是业务数据,业务数据有可能因为各种原因被修改;而自增id是纯技术字段,不参与任何业务逻辑,一旦生成就不应该变。把"业务标识"和"物理主键"分离,是表结构设计里很重要的一个思想。
唯一键(UNIQUE KEY)在练习里容易被忽略,但它和主键一样重要。它意味着该字段的值不能重复。比如学号,如果两个学生填了同一个学号,数据库应该拒绝。没有唯一键,重复数据就会悄悄进入表里,等到统计时才发现数据对不上。
非空约束(NOT NULL)和默认值(DEFAULT)则是在入口处帮助过滤脏数据。比如gender字段,如果用户没填性别,与其存一个NULL,不如给一个默认值0,这样后面统计时不需要IFNULL处理,逻辑会清爽很多。
我当时练了三次左右才把这些约束用熟,每次重敲一遍,理解就深一层。这里提醒一下:这阶段最好用命令行操作,别急着上图形化工具。命令行能让你把每条SQL的原貌看清楚,图形化工具很容易把细节隐藏掉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. INSERT写入:往表里塞数据远比想象中讲究
数据库和表立起来之后,第三章接下来是怎么把数据写进去。INSERT语法本身很简单,但真正动手时你会发现很多细节要处理。
2.1 单条插入与批量插入的效率差异
最基本的写法:
sql复制INSERT INTO student (student_no, name, gender, birthday, score)
VALUES ('20260001', '张三', 1, '2005-06-15', 88.5);
你会发现我没写id和created_at。因为id是自增,created_at有默认值CURRENT_TIMESTAMP,这两个字段在插入时可以不指定,让数据库自己填。这也是"有默认值的字段尽量不手动填"的体现——少填一个字段,就少一个出错的入口。
如果有多条数据要插入,有两种写法。第一种是逐条执行:
sql复制INSERT INTO student (student_no, name, gender, birthday, score) VALUES ('20260002', '李四', 0, '2005-08-20', 91.0);
INSERT INTO student (student_no, name, gender, birthday, score) VALUES ('20260003', '王五', 1, '2005-02-11', 76.5);
INSERT INTO student (student_no, name, gender, birthday, score) VALUES ('20260004', '赵六', 0, '2005-11-30', 83.0);
第二种是拼成一条:
sql复制INSERT INTO student (student_no, name, gender, birthday, score)
VALUES
('20260002', '李四', 0, '2005-08-20', 91.0),
('20260003', '王五', 1, '2005-02-11', 76.5),
('20260004', '赵六', 0, '2005-11-30', 83.0);
两种写法在功能上等价,但批量插入的效率明显更高。原因在于MySQL每执行一条INSERT,都会产生一次日志记录、一次索引更新,批量插入可以把这些开销合并。本地练习时感觉不出来,但以后数据量上来、线上环境里,批量插入是最基本的优化手段之一。
批量插入时需要注意:VALUES后面的多行之间用逗号分隔,最后一行用分号。写的时候最好一行一组,既方便检查,又不容易出错。字段顺序可以打乱,但VALUES里的值必须和字段列表一一对应。
2.2 时间、字符串和NULL:最容易翻车的三个输入点
首先是时间字段。MySQL里插入时间有两种方式,一种是直接写字符串:
sql复制INSERT INTO student (student_no, name, birthday) VALUES ('20260005', '小周', '2006-01-01');
只要格式是'YYYY-MM-DD',MySQL会自动转换。另一种是用函数:
sql复制INSERT INTO student (student_no, name, created_at) VALUES ('20260006', '小吴', NOW());
NOW()返回当前系统的日期时间。当时我很困惑:为什么有时候用CURRENT_TIMESTAMP,有时候用NOW()?其实这两个函数的结果是一样的,NOW()是函数写法,CURRENT_TIMESTAMP是关键字写法,在INSERT和UPDATE的默认值场景里甚至可以不加括号直接用。选择哪个纯粹是习惯问题。如果插入时不知道时间,也可以干脆不填这个字段,让建表时设置的DEFAULT CURRENT_TIMESTAMP自动填上当前时间,这样每行记录都有一个客观的创建时间,不用依赖INSERT语句的准确度。
其次是字符串。VARCHAR字段插入时一定要加引号,不加引号MySQL会把值当成列名或数字去解析。比如name字段写成INSERT INTO student (name) VALUES (王五);,非严格模式下MySQL可能把它当字符串处理但会报警告,严格模式下直接报错。初学者最好开严格模式,让MySQL把所有问题暴露出来,而不是悄悄容忍。
最后是NULL。如果字段定义了NOT NULL,插入NULL会直接报错。这是好事,因为它把异常数据挡在了外面。如果字段没有NOT NULL约束,插入时会允许NULL,但NULL在查询里不等于空字符串'',也不等于0,它表示"未知"。这个语义上的差异,后面学WHERE条件时会经常遇到——WHERE name = NULL永远查不到数据,要写WHERE name IS NULL。
3. UPDATE和DELETE:改数据之前,先记住这三个教训
第三章里我最深刻的部分其实是UPDATE和DELETE,因为这两条语句的"杀伤力"比INSERT和SELECT大得多。它们让我第一次意识到,SQL里的一个键盘失误,可能瞬间毁掉整张表的数据。
3.1 忘写WHERE的代价:一次UPDATE毁掉整张表
UPDATE的语法结构是:
sql复制UPDATE student SET score = 90 WHERE id = 1;
其中WHERE条件是用来限定"目标行"的。如果忘了写WHERE:
sql复制UPDATE student SET score = 90;
这条语句会把表里所有学生的score都改成90。练习环境里无所谓,生产环境就是事故。我当时练习时就在一个模拟表上忘了写WHERE,幸好只是练习数据,但从那以后养成了一个习惯:执行UPDATE之前,先写一条SELECT,用同样的WHERE条件看一眼这到底筛出了哪些行。
sql复制SELECT id, student_no, name, score FROM student WHERE id = 1;
-- 确认无误后再执行
UPDATE student SET score = 90 WHERE id = 1;
这个习惯成本很低,但能救命。别嫌麻烦,尤其在你还不确定WHERE条件写对了没有的时候。
3.2 UPDATE多字段的写法与事务保护
更新多个字段时,字段之间用逗号分隔:
sql复制UPDATE student
SET score = 92.5, birthday = '2005-05-20'
WHERE id = 2;
这里有一个初学者经常踩的坑:SET后面不能用AND连接,不能用SET score = 92.5 AND birthday = '2005-05-20'这种写法。这会被解析成对布尔表达式的计算,结果是0或1,最终写进score字段,和你的本意完全相反。
执行完UPDATE之后,MySQL会返回Query OK, 1 row affected,这个数字表示实际被修改的行数。如果返回0 rows affected,说明WHERE条件没有匹配到任何行,或者匹配到的行的值没有发生变化。所以执行UPDATE后看返回信息,是判断操作是否生效最快的办法。
更保险的做法是用事务把UPDATE包起来:
sql复制START TRANSACTION;
UPDATE student SET score = 92.5 WHERE id = 2;
-- 先不提交,查询确认结果
SELECT * FROM student WHERE id = 2;
-- 确认无误
COMMIT;
-- 如果发现不对
-- ROLLBACK;
事务的意义在于让"修改"变成一个可回退的操作。你可以在COMMIT之前反复检查,发现问题就ROLLBACK回原点。第三章一般不会讲太深的事务,但我建议从第一次执行UPDATE就养成这个习惯,后面学事务时就会非常自然。
3.3 DELETE与TRUNCATE:删除数据的两条不同路线
DELETE删除指定行:
sql复制DELETE FROM student WHERE id = 9;
同样,忘记WHERE的DELETE会清空整张表。DELETE可以加事务回滚,这是它最大的优势。
TRUNCATE清空整张表:
sql复制TRUNCATE TABLE student;
两者的区别值得一个表格:
| 对比项 | DELETE | TRUNCATE |
|---|---|---|
| 作用范围 | 可以带WHERE删除指定行 | 只能清空整张表 |
| 事务 | 可以回滚 | 不能回滚 |
| 自增计数器 | 不会重置 | 会重置为1 |
| 执行速度 | 慢(逐行删除并记录日志) | 快(直接释放数据页) |
| 空间释放 | 不释放表空间 | 释放表空间 |
我当时用TRUNCATE清空练习表后发现id又回到1了,而用DELETE删完后id还是接着之前的序号走,这就是自增计数器的区别。实际场景里,想清空临时表就用TRUNCATE,想精准删某几行就用DELETE。DROP TABLE则是把整张表连同结构一起删掉,操作前更要仔细确认。
4. SELECT查询:排序、去重和条件过滤的实战理解
SELECT是MySQL里使用频率最高、语法最灵活的部分。第三章不会讲JOIN,但会讲最基础的查询框架:SELECT哪些列、从哪张表、过滤哪些行、怎么排序、怎么分页。这些组合起来,已经能解决不少真实问题。
4.1 SELECT基础框架与执行顺序
先看一个完整的查询:
sql复制SELECT name, score
FROM student
WHERE gender = 1
ORDER BY score DESC
LIMIT 5;
意思是:从student表里,选出gender等于1的行,按score从高到低排序,只取前5行,最后显示name和score两列。
理解这类语句的关键不是记住关键字顺序,而是知道它的执行顺序。SQL写出来是SELECT开头,但数据库真正执行时,顺序是:
- FROM:确定从哪张表取数据
- WHERE:过滤行
- SELECT:确定要输出的列
- ORDER BY:排序
- LIMIT:限制输出行数
所以WHERE条件里不能引用SELECT中定义的别名,因为WHERE执行在SELECT之前。比如你不能写:
sql复制SELECT name, score * 2 AS double_score
FROM student
WHERE double_score > 180;
而应该把计算条件写完整:
sql复制SELECT name, score * 2 AS double_score
FROM student
WHERE score * 2 > 180;
这个顺序问题是我学第三章时最费解的地方,后来手动拆解了几条SQL才彻底明白。建议你也这样试试,在脑子里走一遍执行顺序,后面写复杂查询时特别有帮助。
SELECT里还可以用AS给列起别名。别名只影响查询结果展示,不会改变数据库表结构。有人喜欢去掉AS直接写SELECT name n FROM student,这虽然合法但可读性很差,我建议保留AS。另外,ORDER BY是可以用别名的,因为它在SELECT之后执行,这一点和WHERE形成了鲜明对比。
4.2 ORDER BY与LIMIT:排序和分页的组合
ORDER BY默认是升序ASC,想降序要写DESC。多列排序时,先按第一列排,相同的再按第二列排:
sql复制SELECT student_no, name, score
FROM student
ORDER BY score DESC, student_no ASC;
这个语句会先按分数从高到低排,分数相同再按学号从小到大排。多列排序在真实场景里几乎天天用,比如"先按班级,再按成绩"。
LIMIT的两种写法也值得一提。一种是只写数量:
sql复制SELECT * FROM student LIMIT 3;
另一种是带偏移量:
sql复制SELECT * FROM student LIMIT 3 OFFSET 2;
OFFSET 2表示跳过前2行,然后取3行,等价于LIMIT 2, 3。分页查询就是靠这个实现的:第1页取OFFSET 0,第2页取OFFSET 10,依此类推。注意LIMIT后的数字只能是整数,不能是变量或表达式,但可以用小括号括起来的子查询,只是初学者阶段用不上。
4.3 DISTINCT去重与OR条件:一个常见的认知误区
热搜词里有个问题"mysql的or能去重吗",我一开始看到这个问题时明白为什么有人会问。因为他们写过类似这样的查询:
sql复制SELECT DISTINCT name FROM student WHERE gender = 1 OR gender = 0;
然后把"DISTINCT去重"和"OR条件"混在一起,以为OR会不会影响去重效果。答案是:OR只是条件,去重只由DISTINCT负责。上面这条SQL里,因为gender取0或1几乎覆盖了所有学生,DISTINCT只是把同名的学生去掉而已,和OR没有关系。
真正值得警惕的其实是AND和OR混用时的优先级问题。在MySQL里,AND的优先级高于OR。比如你想查"性别为男且分数大于80,或者性别为女且分数大于90"的学生,写成:
sql复制SELECT * FROM student
WHERE gender = 1 AND score > 80 OR gender = 0 AND score > 90;
因为AND先执行,这个写法其实是对的。但如果你想查"性别为男,或者性别为女且分数大于90":
sql复制SELECT * FROM student
WHERE gender = 1 OR gender = 0 AND score > 90;
这个SQL会被解析成gender = 1 OR (gender = 0 AND score > 90),把所有男生都查出来了。如果这不是你的本意,就必须加括号:
sql复制SELECT * FROM student
WHERE (gender = 1 OR gender = 0) AND score > 90;
所以我的建议是:只要有OR和AND混用,一律用括号明确优先级,不要靠感觉猜。DISTINCT该用就用,但它和OR是两码事,别混在一起记。
5. 常用函数:学完第三章就能直接用的"工具箱"
第三章后半段通常会讲一些MySQL内置函数。函数不需要全部背下来,但要知道"有这么个东西",用到时能想起来去查,这才是学习的正确姿势。
5.1 聚合函数:把多行数据压成一行结果
聚合函数最核心的是COUNT、SUM、AVG、MAX、MIN:
sql复制SELECT COUNT(*) FROM student;
SELECT AVG(score) FROM student;
SELECT MAX(score) AS max_score FROM student;
SELECT MIN(score) AS min_score FROM student;
SELECT SUM(score) FROM student;
这里有一个几乎所有人都会遇到的坑:COUNT()和COUNT(列)的区别。COUNT()统计的是行数,不管该行有没有NULL;COUNT(列)只统计该列不为NULL的行。比如student表有10行,其中3行的gender为NULL,那么COUNT(*)返回10,COUNT(gender)返回7。这个差异在真实统计中非常关键,别用错了。
聚合函数通常和GROUP BY搭配使用,按照某个维度分组后分别统计。比如按性别统计人数:
sql复制SELECT gender
