说起 MySQL 基础语法,我见过不少刚入行的朋友有个误区:觉得语法嘛,无非就是增删改查那几条 SQL,真到面试或者写需求的时候,发现根本不是那么回事。要么是 WHERE 和 HAVING 的顺序搞混,要么是 JOIN 关联出来一堆重复数据,更有甚者一条 UPDATE 忘带条件把整张表改了。这篇就当是一份复习笔记,把 MySQL 最核心的基础语法重新捋一遍。不按教科书的老套路走,而是按我在实际项目和带新人过程中总结出来的“必须掌握、极易踩坑、面试高频”这几个维度来整理。适合准备跳槽刷题的、刚学完数据库基础想巩固的、以及做课程设计或日常开发需要快速查阅的人。
1. 连接数据库与库表管理:复习从环境自查开始
很多人在复习语法的时候,习惯上来就敲 SELECT,结果忽略了最底层那几步。其实连接、建库、建表这些操作,看似基础,却直接决定了你后续所有 SQL 能不能顺利跑起来。我见过太多人在字符集和排序规则上栽跟头,表建完了才发现插入中文数据乱码,回头再改表结构,麻烦得很。
1.1 连接命令与环境准备
命令行连接 MySQL 最常用的就是这个:
bash复制mysql -u root -p
如果是远程连接,加 -h 指主机和 -P 指端口:
bash复制mysql -h 192.168.1.100 -P 3306 -u root -p
连接成功之后,第一件事通常是看当前有哪些数据库:
sql复制SHOW DATABASES;
这个命令的返回值里,information_schema、mysql、performance_schema 这些是系统自带的库,尽量不要去动它们。真正需要操作的是你自己创建的库。
这里有个小习惯我觉得挺重要:每次复习或者入职新公司连数据库时,先执行一下 SELECT VERSION(); 和 SHOW VARIABLES LIKE 'character_set%';。前者确认版本,后者确认字符集配置。MySQL 8.0 和 5.7 在一些细节上有差异,比如 8.0 默认字符集是 utf8mb4,而 5.7 默认是 latin1,这直接关系到你建库时要不要显式指定字符集。
1.2 建库建表时的常见疏忽
建库语法:
sql复制CREATE DATABASE IF NOT EXISTS school DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
这里头的 IF NOT EXISTS 是个好东西,重复执行不会报错,写脚本的时候特别省心。然后 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 是给库设置默认字符集和排序规则。utf8mb4 是完整的 UTF-8,能存 emoji 和生僻字,跟 utf8 比多占了点空间,但现代开发基本都是用 utf8mb4。
选库和建表:
sql复制USE school;
CREATE TABLE students (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age TINYINT UNSIGNED,
email VARCHAR(100) UNIQUE,
class_id INT UNSIGNED,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
建表的时候有几个点我建议你重点复习:
INT UNSIGNED表示无符号整数,范围比普通 INT 大,主键用这个很常见。AUTO_INCREMENT是自增,注意一个表只能有一个自增列,而且必须被索引。VARCHAR(50)里的 50 是字符数不是字节数,这在 utf8mb4 下就是 50 个字符。TINYINT UNSIGNED存年龄合适,范围 0 到 255,别啥都上 INT。ENGINE=InnoDB是事务安全的存储引擎,支持外键和行级锁。除非你有特殊理由,否则就用 InnoDB。TIMESTAMP DEFAULT CURRENT_TIMESTAMP会在插入时自动填当前时间,省去手动维护创建时间的麻烦。
如果你要自己练习,可以先 DROP DATABASE school; 再重建,所谓“破坏式练习”其实很适合复习——建了删、删了建,多来几遍就熟了。当然,生产环境千万别这么干。
1.3 查看表结构与修改表的基本操作
建完表后,随时可以查看表结构:
sql复制DESC students;
或者用更详细的:
sql复制SHOW CREATE TABLE students\G
\G 是命令行客户端特有的,把输出竖着显示,列多的时候比 ; 结尾清晰得多。修改表结构用 ALTER TABLE,像加列、删列、改数据类型:
sql复制ALTER TABLE students ADD COLUMN phone VARCHAR(20) AFTER email;
ALTER TABLE students MODIFY COLUMN age TINYINT NOT NULL DEFAULT 18;
ALTER TABLE students DROP COLUMN phone;
这块我自己的体会是:ALTER TABLE 在生产环境要格外谨慎,大表加列会导致锁表,影响线上读写。复习时知道语法就好,实操上能不频繁改表结构就别改。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 增删改查四件套:把 CRUD 练成肌肉记忆
CRUD 是数据库操作的基本功,也是面试手写 SQL 时最先考的东西。这四个操作本身不难,难的是在各种边界条件下写出正确、安全的语句。我以前帮人排查问题,发现大部分线上事故都出在 UPDATE 和 DELETE 上。
2.1 INSERT:单条、多条与注意事项
最基础的插入:
sql复制INSERT INTO students (name, age, email) VALUES ('张三', 20, 'zhangsan@example.com');
注意这里只写了三个字段,id 自增、created_at 有默认值,都可以不用手动填。字段列表和 VALUES 里的值得一一对应,数量不一致直接报错。
批量插入:
sql复制INSERT INTO students (name, age, email) VALUES
('李四', 21, 'lisi@example.com'),
('王五', 22, 'wangwu@example.com'),
('赵六', 20, 'zhaoliu@example.com');
批量插入比一条条 INSERT 性能好很多,原理是减少了客户端和服务器之间的来回通信次数。我实际测过,插入一万条数据,批量方式比单条快一个数量级不止。如果某条数据重复了,你希望它自动跳过而不是报错中断,可以使用:
sql复制INSERT IGNORE INTO students (name, age, email) VALUES ('张三', 20, 'zhangsan@example.com');
这个在跑初始化脚本时特别好用。
还有个语法需要区分:REPLACE INTO。它的逻辑是如果唯一键冲突,先把旧记录删掉再插入新记录。这个要慎用,因为删除和插入是两件事,不在事务里的话,一旦中间出错数据就丢了。
2.2 SELECT:查询的骨架
SELECT 的完整子句顺序大概是:
sql复制SELECT 字段列表
FROM 表名
WHERE 条件
GROUP BY 分组字段
HAVING 分组后过滤
ORDER BY 排序字段
LIMIT 偏移量, 行数;
这个顺序不只是语法规定,它反映了 SQL 的执行逻辑顺序——先取数据,再过滤,再分组,再排序,最后分页。我发现很多人在复习时忽略这个逻辑顺序,写复杂查询时就会懵。
最基础的查询:
sql复制SELECT * FROM students;
SELECT name, age FROM students WHERE age > 20;
SELECT * 平时练习用没问题,线上环境最好别这么干,显式列出需要的字段,可读性好、传输数据量也小,还能避免表结构变化导致程序出错。
去重用 DISTINCT:
sql复制SELECT DISTINCT age FROM students;
这会把所有不重复的 age 列出来,注意它是作用于整行组合的,不是单列。
2.3 UPDATE 和 DELETE:必须养成的安全习惯
更新语法:
sql复制UPDATE students SET age = 23 WHERE name = '李四';
删除语法:
sql复制DELETE FROM students WHERE name = '李四';
看这两条是不是很简单?但危险恰恰藏在这种简单里。如果你不写 WHERE 条件:
sql复制UPDATE students SET age = 23;
DELETE FROM students;
整张表的 age 全变成 23,或者直接清空整张表。我在公司听过不止一次这种事故,还好最后靠备份恢复。所以我现在养成了一个习惯:任何 UPDATE 或 DELETE 之前,先把对应的 WHERE 条件拿去 SELECT 一遍,确认范围没问题再执行。比如你想更新 id=5 的学生,先 SELECT * FROM students WHERE id = 5;,确认就是这个人,再执行 UPDATE。一分钟的事,能挡下绝大多数手误。
另外提一下 TRUNCATE 和 DELETE 的区别:
sql复制TRUNCATE TABLE students;
TRUNCATE 是快速清空整张表,不逐行删除,也不记录每行的删除日志,所以速度极快,但它不可按条件过滤、不可回滚(在大多数情况下)。DELETE 是可以加 WHERE、可以有事务回滚的。复习时这个区别经常被问到,值得记牢。
3. WHERE、ORDER BY、GROUP BY、HAVING:筛选逻辑的复习重点
我刚入行那会儿,写 SQL 经常被分组和过滤搞得头大。后来才发现,只要把每个子句的执行时机搞清楚,谜团就自然解开了。这一部分在面试里出现的频率很高,值得投入精力重点过一遍。
3.1 WHERE 与 HAVING 的本质区别
这两个都用于过滤,但作用于不同的阶段:
WHERE是在分组之前就过滤,针对的是 FROM 出来的原始行数据。HAVING是在分组之后过滤,针对的是 GROUP BY 产生的分组结果。
看个例子:
sql复制SELECT class_id, COUNT(*) AS student_count
FROM students
WHERE age >= 20
GROUP BY class_id
HAVING COUNT(*) >= 2;
这条 SQL 的意思是:先只保留 age 大于等于 20 的学生,再按班级分组,最后只要那些班级人数大于等于 2 的分组。注意 WHERE 不能使用聚合函数,HAVING 可以。如果你试过 WHERE COUNT(*) > 1,MySQL 会直接报错,因为 WHERE 执行时聚合结果根本还不存在。
还有一点:HAVING 后面可以用别名,WHERE 不行。像这样是可以的:
sql复制SELECT class_id, COUNT(*) AS cnt FROM students GROUP BY class_id HAVING cnt > 1;
而这样会报错(在严格模式下):
sql复制SELECT * FROM students WHERE age > 20 GROUP BY age; -- 这个可以
SELECT class_id, COUNT(*) AS cnt FROM students WHERE cnt > 1 GROUP BY class_id; -- 这个不行
3.2 ORDER BY 与 LIMIT 的搭配细节
排序是日常查询里几乎必用的:
sql复制SELECT name, age FROM students ORDER BY age DESC;
SELECT name, age FROM students ORDER BY age DESC, id ASC;
第一个按年龄从大到小排,第二个先按年龄降序,年龄相同的按 id 升序。多列排序时,写在前面的优先级高。
分页查询:
sql复制SELECT * FROM students ORDER BY id LIMIT 10 OFFSET 20;
这里的 OFFSET 20 表示跳过前面 20 行,LIMIT 10 表示从第 21 行开始取 10 行。老写法是 LIMIT 20, 10,意思一样,但可读性差点,我建议新手就用 OFFSET 的写法,语义更明确。
有个细节:LIMIT 只有在有 ORDER BY 时才有稳定意义。不排序直接 LIMIT,返回的行顺序是不确定的,这是 MySQL 不保证的行为。所以在做分页时一定要带 ORDER BY,而且最好按唯一字段排序,否则某两行顺序互换会导致分页数据重复或遗漏。
3.3 NULL 的判断与空值陷阱
数据库里的 NULL 不是 0,也不是空字符串,它代表“未知”。所有跟 NULL 的算术运算结果都是 NULL:
sql复制SELECT NULL + 1; -- 结果是 NULL
判断 NULL 不能用 = 或 !=,要用 IS NULL 或 IS NOT NULL:
sql复制SELECT * FROM students WHERE email IS NULL;
SELECT * FROM students WHERE email IS NOT NULL;
这里一个常见误区就是写 WHERE email = NULL,查出来永远是空。另一个是忽略 NULL 对聚合函数的影响——COUNT(字段) 不统计 NULL 值,COUNT(*) 统计所有行。所以如果你要数有多个人填了邮箱,写 COUNT(email);要数总共有多少人,写 COUNT(*)。
NULL 对 NOT IN 的影响也是经典面试题。比如:
sql复制SELECT * FROM students WHERE class_id NOT IN (SELECT id FROM classes);
如果子查询返回的结果中包含 NULL,这个查询可能什么也查不出来。因为 NOT IN 逻辑上等价于 <> ALL,而跟 NULL 比较结果全是未知。遇到这种场景,要么在子查询加 WHERE id IS NOT NULL,要么改用 NOT EXISTS。
4. 多表关联:JOIN 与子查询的实用选型
在真实业务里,数据几乎不会只放在一张表里。学生表和班级表、订单表和用户表、商品表和分类表,都是典型的一对多关系。复习多表关联,重点不是背 JOIN 的语法格式,而是理解什么时候用哪种 JOIN、什么时候用子查询、两者性能差别在哪。
4.1 三种常用 JOIN 的直观理解
假设有两张表:
sql复制-- 班级表 classes
CREATE TABLE classes (
id INT UNSIGNED PRIMARY KEY,
name VARCHAR(50)
);
-- 学生表 students 增加 class_id 字段
ALTER TABLE students ADD COLUMN class_id INT UNSIGNED AFTER email;
内连接:
sql复制SELECT students.name, classes.name AS class_name
FROM students
INNER JOIN classes ON students.class_id = classes.id;
INNER JOIN 只返回两边都能匹配上的行。学生表里如果有的学生没有 class_id,或者班级表里有班级没学生,这些行都不会出现在结果里。
左连接:
sql复制SELECT students.name, classes.name AS class_name
FROM students
LEFT JOIN classes ON students.class_id = classes.id;
LEFT JOIN 返回左边表(students)的所有行,右边表匹配不上的地方补 NULL。这个特别常用,比如“列出所有学生及其班级,没分班的也要列出来”。
右连接:
sql复制SELECT students.name, classes.name AS class_name
FROM students
RIGHT JOIN classes ON students.class_id = classes.id;
RIGHT JOIN 返回右边表(classes)的所有行,左边匹配不上补 NULL。实际开发中用得少一些,因为把表顺序换一下就能用 LEFT JOIN 替代,但面试时经常被问到三者区别。
我在复习时喜欢用一个小类比:INNER JOIN 是两个圈的交集,LEFT JOIN 是左边圈的全部加上交集部分,RIGHT JOIN 是右边圈的全部加上交集部分。这样记特别快。
4.2 JOIN 的坑:条件写错导致笛卡尔积
JOIN 最容易出的问题是忘了写 ON 条件或者 ON 条件写得不对,导致结果爆量:
sql复制SELECT * FROM students INNER JOIN classes;
这条 SQL 没写 ON 条件,MySQL 会把两张表做笛卡尔积——如果有 5 个学生、3 个班级,结果就是 15 行。这显然不是你要的。还有一个隐蔽的坑:关联字段有重复值。如果 students 表里有多行 class_id 都等于某个班级 id,那 JOIN 结果中该班级会跟每个学生都组合一遍,出现重复数据。
所以写 JOIN 时,我的习惯是先想清楚两张表关联字段的基数:是一对一、一对多还是多对多。多对多的时候,通常需要中间表才能正确关联。
4.3 子查询与 EXISTS 的取舍
子查询有两种常见形式:标量子查询和表子查询。
标量子查询返回单个值:
sql复制SELECT name, age FROM students
WHERE age > (SELECT AVG(age) FROM students);
表子查询配合 IN:
sql复制SELECT name FROM students
WHERE class_id IN (SELECT id FROM classes WHERE name = '一班');
子查询写起来直观,但性能不一定好。MySQL 5.7 及之前版本里,有些子查询的优化并不理想,特别是 IN 子查询有时候可以改写成 JOIN 来提速。我用过一个经验法则:如果子查询结果集很小,IN 没问题;如果外层表很小而子查询很大,EXISTS 通常更效率。
EXISTS 的写法:
sql复制SELECT name FROM students s
WHERE EXISTS (
SELECT 1 FROM classes c WHERE c.id = s.class_id AND c.name = '一班'
);
注意 EXISTS 子查询里我写的是 SELECT 1,不是 SELECT *。因为 EXISTS 只关心子查询有没有返回行,不关心返回什么列,写 1 更省资源。这也是 MySQL 中一个常被问到的优化细节。
4.4 UNION 与 UNION ALL
多表关联说的是列的方向合并,而 UNION 是行的方向合并。两条 SELECT 的结果上下拼在一起:
sql复制SELECT name FROM teachers
UNION
SELECT name FROM students;
SELECT name FROM teachers
UNION ALL
SELECT name FROM students;
区别在于:UNION 会去重,UNION ALL 不去重。去重是有代价的,MySQL 需要额外做排序或哈希去重。如果确认两组数据不会重复,直接用 UNION ALL,性能好很多。
注意 UNION 要求两条 SELECT 的列数相同,数据类型要兼容。列名以第一条 SELECT 为准。
5. 索引、约束与事务:从“能跑”到“靠谱”
基础语法复习到后面,光会写 CRUD 是不够的。一个表的数据量上了百万级,没有索引的查询会慢到让你怀疑人生。这一节我主要讲三件事:索引怎么用、约束怎么设、事务是什么。这是从“能跑”到“靠谱”的过渡阶段,也是面试中拉开差距的地方。
5.1 索引的创建与常见误区
创建索引:
sql复制CREATE INDEX idx_students_age ON students(age);
CREATE UNIQUE INDEX idx_students_email ON students(email);
也可以在 CREATE TABLE 时直接定义。索引最大的价值是加速查询,原理类似于书的目录——没有目录,要找某个内容就得翻遍全书;有了目录,直接定位到页码。
但索引不是越多越好,它也有代价:每次 INSERT、UPDATE、DELETE 时,索引也要同步更新,额外的写入开销可能导致写操作变慢。所以只给那些经常出现在 WHERE 条件、ORDER BY、JOIN ON 字段上的列建索引。
另一个常见误区是“索引列上做了函数运算就没用了”。比如:
sql复制SELECT * FROM students WHERE YEAR(created_at) = 2024;
如果 created_at 上有索引,这个查询也无法用上,因为 MySQL 必须先算函数值才能比较。改成范围查询:
sql复制SELECT * FROM students WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
这样就能走索引了。
5.2 约束:主键、外键、唯一约束
主键约束是最基础的,它隐含非空和唯一两个特性。一个表只能有一个主键,但可以联合多个字段做复合主键:
sql复制CREATE TABLE course_student (
course_id INT UNSIGNED,
student_id INT UNSIGNED,
score DECIMAL(5,2),
PRIMARY KEY (course_id, student_id)
);
外键约束用来保证引用完整性:
sql复制ALTER TABLE students
ADD CONSTRAINT fk_students_class
FOREIGN KEY (class_id) REFERENCES classes(id);
加上这行之后,如果 class_id 在班级表中不存在,INSERT 会直接报错。外键在数据一致性要求高的场景很有用,但也会带来额外的检查和性能开销。很多互联网公司的高并发场景反而刻意不用外键,把完整性交给应用层保证。考试和课程设计通常会考外键,实际工作中用不用看项目约定。
唯一约束:
sql复制ALTER TABLE students ADD CONSTRAINT uk_students_email UNIQUE (email);
作用是不允许两行数据有相同的 email,跟主键的区别是唯一约束允许 NULL(MySQL 里多个 NULL 是允许的),主键不行。登录注册场景特别常用。
5.3 事务:ACID 与基本操作
事务是一组要么全成功、要么全失败的数据库操作。经典的转账场景:
sql复制START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
如果第二条 UPDATE 出错,可以 ROLLBACK; 回滚,第一条的扣款也会被撤销。这样就不会出现“扣了钱但对方没收到”的情况。
事务有四个特性,简称 ACID:
- 原子性(Atomicity):操作要么全做要么全不做。
- 一致性(Consistency):事务前后数据都处于合法状态。
- 隔离性(Isolation):并发事务互不干扰。
- 持久性(Durability):提交后修改永久生效。
InnoDB 默认是自动提交的,也就是每条语句单独成一个事务。复习事务时动手写一下 START TRANSACTION、COMMIT、ROLLBACK 的组合,比单纯背概念印象更深刻。
隔离级别这块,重点要知道 MySQL 默认为 REPEATABLE READ。它解决了不可重复读的问题,但在某些情况下仍可能出现幻读。其实 InnoDB 通过间隙锁在 REPEATABLE READ 级别下已经把幻读基本消除了,这也是 MySQL 的一个特殊之处。面试被问到时,能说出这一点会是加分项。
6. 复习阶段的命令速查与高频问题清单
到了复习的收尾阶段,我觉得最有价值的是把零散的知识点凝成一张“速查表”和一份“避坑清单”。下面这些内容来自我平时排查问题和带新人时的总结,按主题分类,方便你快速翻阅。
6.1 常用语法速查表
| 操作 | 示例 |
|---|---|
| 连接数据库 | mysql -u root -p |
| 查看所有库 | SHOW DATABASES; |
| 切换库 | USE school; |
| 查看所有表 | SHOW TABLES; |
| 查看表结构 | DESC students; |
| 插入数据 | INSERT INTO students (name, age) VALUES ('张三', 20); |
| 批量插入 | INSERT INTO t (c1, c2) VALUES (1, 2), (3, 4), (5, 6); |
| 条件查询 | SELECT * FROM students WHERE age >= 20; |
| 排序 | SELECT * FROM students ORDER BY age DESC; |
| 分组统计 | SELECT class_id, COUNT(*) FROM students GROUP BY class_id; |
| 分页 | SELECT * FROM students ORDER BY id LIMIT 10 OFFSET 20; |
| 更新数据 | UPDATE students SET age = 23 WHERE id = 5; |
| 删除数据 | DELETE FROM students WHERE id = 5; |
| 清空表 | TRUNCATE TABLE students; |
| 删除表 | DROP TABLE students; |
| 创建索引 | CREATE INDEX idx_name ON students(age); |
| 开启事务 | START TRANSACTION; |
| 提交事务 | COMMIT; |
| 回滚事务 | ROLLBACK; |
6.2 高频问题与错误排查
1. 忘记 WHERE 条件,更新或删除了全表数据。 这是最严重的事故,预防方法就是我在第 2 节说的:先 SELECT 再 UPDATE/DELETE。
2. 字符集设置不对,中文乱码。 建库的时候明确指定 DEFAULT CHARACTER SET utf8mb4。如果数据已经乱了,可以尝试 ALTER DATABASE school CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,但这只影响新数据,旧数据恢复起来要复杂得多。所以一开始就设对,别偷懒。
3. SELECT * FROM t WHERE email = NULL 查不出数据。 应该用 IS NULL。这是 NULL 语义最经典的坑。
4. GROUP BY 和 SELECT 字段不匹配。 MySQL 的 ONLY_FULL_GROUP_BY 模式下,SELECT 后面的非聚合列必须出现在 GROUP BY 里。比如:
sql复制SELECT name, age, COUNT(*) FROM students GROUP BY class_id;
这条会报错,因为 name 和 age 既不在 GROUP BY 里,也不是聚合函数参数。在非严格模式或 MySQL 5.7 之前可能不报错,但返回结果是随机的。复习时最好打开 ONLY_FULL_GROUP_BY,按严格模式来写。
5. JOIN 忘写条件导致笛卡尔积。 写完 JOIN 检查一下有没有 ON,以及 ON 关联字段是否正确。
6. LIMIT 10, 20 和 LIMIT 20 OFFSET 10 搞混。 前一个是从第 11 行开始取 20 行,后一个是从第 11 行开始取 10 行。我建议统一用 LIMIT 行数 OFFSET 偏移量 的写法,避免歧义。
7. 事务忘记提交。 开了 START TRANSACTION 之后,如果一直没 COMMIT 或 ROLLBACK,事务会一直持有锁,其他会话可能被阻塞。复习时可以故意不提交,再开一个窗口查询试试,就明白锁是什么感受了。
6.3 数据备份与恢复的基本功
基础语法的复习往往把备份恢复忽略掉了,但这个在真实工作中太关键了。命令行备份:
bash复制mysqldump -u root -p school > school_backup.sql
恢复:
bash复制mysql -u root -p school < school_backup.sql
mysqldump 生成的 SQL 文件里包含建表和 INSERT 语句,用重定向导入即可。大数据库备份还有增量备份、binlog 恢复等高级玩法,但对复习基础语法来说,掌握全量备份和恢复就够了。
6.4 给复习者的一点建议
结合我带人和自己学习的经验,最后想多说几句:复习 MySQL 基础语法,光看不行,必须动手。你可以自己建一个本地库,造几百条模拟数据,把今天复习到的语法挨个跑一遍,尤其是故意写错一些语句,看看 MySQL 报什么错。我在复习的时候会给自己出题,比如“统计每个班级中年龄大于 20 的学生人数并按人数从多到少排序”,然后动手写,写完再对比标准写法找差异。这个方法看着笨,但对加深理解非常有帮助。多写、多错、多总结,语法基础才能真正扎实。
