很多人以为学 MySQL 就是背几条 SQL 语句,其实真不是这个路子。这期内容是我这个系列笔记的第二天,标题叫“Day2-MySQL-SQL-1”,延续第一天的环境准备,从今天开始正式碰 SQL 语句。如果你已经装好了 MySQL 8.x,能用命令行或者 Workbench 连上数据库,但又搞不清接下来该学什么、怎么学,这篇文章就是按我的真实教学节奏整理出来的。SQL 是数据库的通用语言,MySQL 只是其中一种实现,把这套基础打牢,后面去用 SQL Server、PostgreSQL 也只是换皮不换骨。
我一直觉得 SQL 入门最大的障碍不是语法难,而是不知道一条查询语句从写出来到拿到结果,中间到底经历了什么。所以这期内容我刻意把节奏放慢,不贪多,就讲清楚查询、函数、关联、写数据这几个最基本的动作。每段都会配完整的 SQL 示例和实际执行时容易踩的坑。已经有开发经验但没系统梳理过 SQL 语法的朋友,同样可以直接照着查漏补缺。
我自己的经验是,SQL 基础阶段最重要的不是刷题,而是建立一个“先筛选、再加工、后输出”的思维模型。这个思维一旦立住了,后面学窗口函数、学慢 SQL 优化,都会觉得特别顺。下面进入正题。
1. 第二天学什么:SQL 语言的整体地图
先看一下这节在整个学习路径里的位置。第一天我们把 MySQL 下载、安装、初始化、客户端连接这些环境问题解决了,等于已经把“房子”盖好了,但房子里怎么住人、东西怎么摆,这才是 SQL 要解决的事。
1.1 SQL 在数据库里到底扮演什么角色
SQL 全称是 Structured Query Language,结构化查询语言。它不是某个软件独有的,而是关系型数据库的通用操作规范。MySQL、Oracle、SQL Server、PostgreSQL 都支持 SQL,只不过各自在细节语法上有一些小差别。你可以把 SQL 理解成“服务员点单”,你不需要知道后厨(存储引擎)是怎么炒菜的,只要把需求说清楚,数据库就能按规则把数据取回来或者改好。
学 SQL 的真正意义在于:它是目前数据领域唯一一门“全岗位通用”的语言。后端开发要写 SQL 查数据,数据分析师要写 SQL 做报表,DBA 要写 SQL 做运维,甚至产品经理也需要能看懂 SQL 日志。所以这个投入回报率极高,且没有替代品。
1.2 SQL 语言家族的五大分类
SQL 不是一个单一的命令,而是由几类功能完全不同的语句组成的集合。我整理成一张表,方便你按图索骥:
| 分类 | 全称 | 代表语句 | 作用 |
|---|---|---|---|
| DQL | Data Query Language | SELECT | 查询数据,用的最多 |
| DML | Data Manipulation Language | INSERT、UPDATE、DELETE | 增删改记录 |
| DDL | Data Definition Language | CREATE、ALTER、DROP | 定义库表结构 |
| DCL | Data Control Language | GRANT、REVOKE | 控制权限 |
| TCL | Transaction Control Language | COMMIT、ROLLBACK | 事务控制 |
Day2 这节“SQL-1”重点落在 DQL 上,也就是 SELECT 查询。因为查询是使用频率最高的操作,而且学会查询之后,你对表的字段设计、数据分布会有直觉,再去学增删改就会轻松很多。DDL 我们在第一天建库建表时已经基本接触过了,后面再逐步深入。
1.3 这门课推荐的练手方式:一张表走天下
很多教程喜欢一上来就扔出五六张表,学生还没搞懂字段关系,就被 JOIN 吓退了。我的建议完全反过来:建一张足够贴近真实场景的学生表,把各种查询语法都在这张表上跑一遍,跑熟了再逐步拆出第二张、第三张表。
我接下来演示用的表结构是这样的:
CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT 'M' COMMENT '性别', age INT COMMENT '年龄', score DECIMAL(5,2) COMMENT '考试成绩', class_id INT COMMENT '班级编号', enroll_date DATE COMMENT '入学日期' );再批量插入一些模拟数据,后续所有示例就都有了操作对象。这个方法我试过很多次,比漫无目的刷题高效得多。
2. 把查询玩明白:SELECT 从语法到执行顺序
SELECT 是 SQL 里最核心的语句,没有之一。但很多人写 SELECT 就是机械地把字段列出来,根本不理解这些子句的执行顺序。真正常用之后你会发现,很多查询结果不对,都是因为对执行顺序理解有偏差。
2.1 SELECT 语法的完整拼装顺序
一条完整的查询语句通常是这个形态:
SELECT 字段列表 FROM 表名 WHERE 过滤条件 GROUP BY 分组字段 HAVING 分组后过滤 ORDER BY 排序字段 LIMIT 行数限制;这里我想强调一个初学者最容易忽略的点:书写的顺序是这么写,但数据库执行 SQL 的时候不是从上往下执行的。实际的逻辑执行顺序大致是:
- FROM:确定从哪张表取数
- WHERE:过滤掉不符合条件的行
- GROUP BY:对过滤后的行做分组
- HAVING:过滤分组后的结果
- SELECT:计算要返回的字段和表达式
- ORDER BY:对最终结果排序
- LIMIT:限制返回行数
这个顺序至关重要。举个例子,如果你在 WHERE 里给聚合函数起别名然后去过滤,比如 WHERE avg_score > 60,一定会报错,因为 WHERE 执行时 SELECT 里的别名还不存在。这类问题我后面还会细讲。
2.2 基础查询示例:字段、别名、去重
最简单的查询就是把字段原样拿出来:
-- 查询所有学生的姓名和成绩 SELECT name, score FROM student; -- 给字段起别名,方便阅读 SELECT name AS '姓名', score AS '成绩' FROM student; -- 查询有哪些班级编号,去掉重复值 SELECT DISTINCT class_id FROM student;这里有个小细节:别名的 AS 可以省略,写成SELECT name '姓名'也能跑,但可读性很差,我不建议省略。DISTINCT 的去重范围是它后面所有字段的组合,比如DISTINCT class_id, gender去重的是这两个字段的联合值,不是只看 class_id 这一个字段。
2.3 WHERE 条件过滤:比较、逻辑、模糊匹配
WHERE 是查询里最常用的过滤工具。常用的运算符我分为四类:
- 比较运算符:
=、>、<、>=、<=、!=或<> - 逻辑运算符:
AND、OR、NOT - 范围判断:
BETWEEN ... AND ...、IN (...)、IS NULL - 模糊匹配:
LIKE,配合%和_通配符
实际写几条看一下:
-- 查询年龄大于18岁且成绩及格的学生 SELECT name, age, score FROM student WHERE age > 18 AND score >= 60; -- 查询班级编号为 1 或 2 的学生 SELECT name, class_id FROM student WHERE class_id IN (1, 2); -- 查询姓名以"张"开头的学生 SELECT name FROM student WHERE name LIKE '张%'; -- 查询出生日期在指定区间内的学生 SELECT name, enroll_date FROM student WHERE enroll_date BETWEEN '2023-01-01' AND '2023-12-31';LIKE 里的%代表任意长度字符,_代表单个字符。LIKE '张%'匹配所有姓张的,LIKE '张_'只匹配姓张且名字只有一个字的人。这里需要注意:如果字段本身含有%或_这样的特殊字符,需要用 ESCAPE 关键字指定转义符,我在实际项目中就遇到过搜索功能查不到含下划线用户名的坑。
还有一个高频坑:NULL 的判断。新手特别容易写成WHERE score = NULL,这样永远查不到任何数据。因为 NULL 不是一个值,它代表“未知”,不能参与等值比较,必须写成WHERE score IS NULL或者WHERE score IS NOT NULL。
2.4 ORDER BY 排序与 LIMIT 分页:细节藏在边界里
排序语法本身很简单,ORDER BY score DESC就是按成绩降序,ASC是升序且可以省略。真正容易出问题的是 NULL 值的排序位置。在 MySQL 中默认情况下,升序排序 NULL 排在最前面,降序排序 NULL 排在最后面。如果你想让 NULL 固定排在最后,可以用:
SELECT name, score FROM student ORDER BY ISNULL(score), score DESC;LIMIT有两种写法:LIMIT n表示取前 n 条;LIMIT offset, n表示跳过 offset 条后再取 n 条,这是分页查询的核心。比如每页展示 10 条,第 3 页就是LIMIT 20, 10。但这里有个性能问题值得你从第一天就记住:LIMIT 的 offset 越大,查询越慢,因为数据库要先扫描并丢弃前面那么多行。后面学到索引优化时会再展开。
3. 函数与分组统计:从一行数据到一组结论
学会了基础查询,等于拿到了数据,但我们日常需求往往是“统计一下每个班有多少人”“算出平均成绩”,这就涉及到 MySQL 内置函数和分组聚合。这块是 SQL 从“取值”到“分析”的转折点,也通常是初学者第一次产生“卧槽还能这样”感觉的地方。
3.1 常用单行函数:字符串、数值、日期
单行函数的意思是对每一行数据单独处理并返回一个结果值。常用的我用表列一下:
| 类别 | 函数示例 | 作用 |
|---|---|---|
| 字符串 | CONCAT(name, age) | 拼接多个字段 |
| 字符串 | SUBSTRING(name, 1, 1) | 截取子串 |
| 字符串 | UPPER(name) / LOWER(name) | 大小写转换 |
| 字符串 | TRIM(name) | 去除首尾空格 |
| 数值 | ROUND(score, 1) | 四舍五入保留指定位数 |
| 数值 | FLOOR(score) / CEILING(score) | 向下/向上取整 |
| 日期 | NOW() | 当前时间 |
| 日期 | DATE_FORMAT(date, '%Y-%m-%d') | 格式化日期 |
| 日期 | DATEDIFF(date1, date2) | 计算两个日期相差天数 |
举一个我在实际中经常用的例子:查询学生姓名长度超过 3 个字符的记录,可以直接把 LENGTH 函数用在 WHERE 里:
SELECT name FROM student WHERE LENGTH(name) > 3;这里提醒一个中文相关的坑:MySQL 的LENGTH()是按字节计算的,一个 UTF-8 编码的中文占 3 个字节。所以如果你本意是想找出名字超过 3 个“字”的学生,这里就会出错,应该用CHAR_LENGTH(name)按字符计算。
3.2 聚合函数与 GROUP BY:分组统计的核心写法
聚合函数的最大特点是“多进一出”,把一组的行聚合成一个汇总值。五个最常用的分别是:COUNT()计数、SUM()求和、AVG()求平均、MAX()最大值、MIN()最小值。它们很少单独使用,几乎总是配合GROUP BY一起出现。看两个例子:
-- 统计每个班的人数和平均分 SELECT class_id AS '班级', COUNT(*) AS '人数', AVG(score) AS '平均分' FROM student GROUP BY class_id; -- 统计男生和女生的最高分、最低分 SELECT gender AS '性别', MAX(score) AS '最高分', MIN(score) AS '最低分' FROM student GROUP BY gender;这里有个很多人没想明白的点:GROUP BY之后,SELECT 后面能出现的字段是有严格限制的。只能出现两类字段:一是被 GROUP BY 的字段本身,二是聚合函数计算出来的结果。比如SELECT name, AVG(score) FROM student GROUP BY class_id这句,MySQL 在开启了 ONLY_FULL_GROUP_BY 模式(默认开启)时会直接报错,因为 name 字段既不参与分组,也不是聚合函数。但从逻辑上讲,一个组里有多个学生,你到底想让数据库返回哪个 name?本身就是个歧义。
3.3 WHERE 与 HAVING 的分工:先过滤再分组
初学者最容易搞混的就是 WHERE 和 HAVING 的区别。我的记忆口诀是:WHERE 是分组前过滤行,HAVING 是分组后过滤组。WHERE 不能使用聚合函数,HAVING 专门用来配合聚合函数做条件筛选。
一个典型场景:统计每个班级平均分,但只看平均分大于等于 80 分的班级:
SELECT class_id AS '班级', AVG(score) AS '平均分' FROM student GROUP BY class_id HAVING AVG(score) >= 80;如果你想把 HAVING 的条件换成 WHERE AVG(score) >= 80,同样会报错,原因就是 WHERE 执行阶段聚合函数的结果还没算出来。我再补充一个真实的效率经验:能用 WHERE 过滤掉的记录,千万别留到 HAVING 再筛。原因很好理解,先 WHERE 可以把参与分组的数据量大幅减少,分组计算的压力自然就小了。我在给线上慢查询做优化时,不少问题就是 HAVING 里写了本该属于 WHERE 的条件。
4. 多表关联与子查询:SQL 真正值钱的部分
单表查询练得再熟,也只是 SQL 的地基。实际开发中业务数据几乎不会只存在一张表里:用户表、订单表、商品表、日志表都是分开设计的。把多张表按关联字段拼在一起取数,这是从“会写 SQL”到“会用 SQL 解决问题”的关键一步。
4.1 为什么需要拆表和关联
数据库设计时要遵循范式,尽量避免数据的冗余存储。比如学生表和班级表,如果把所有班级信息都塞进学生表,同样一个班级名要重复存储好多次,既浪费空间,又容易在改名时产生不一致。正确做法是拆成两张表,用 class_id 作为关联字段。
为了演示,我加一张班级表:
CREATE TABLE class ( id INT PRIMARY KEY COMMENT '班级编号', class_name VARCHAR(50) COMMENT '班级名称' );这时候如果你直接写SELECT * FROM student, class,就会得到两张表的“笛卡尔积”,也就是每种组合都出现一遍。现实中这种不带关联条件的连表查询几乎都是事故级错误,因为数据量会爆炸式增长。正确做法永远是用关联条件去限制组合关系。
4.2 三种常用 JOIN:内连接、左连接、右连接
JOIN 就是把多张表“拼”起来的语法。我习惯用集合图来理解:内连接(INNER JOIN)取两表的交集,左连接(LEFT JOIN)保留左表全部记录,右连接(RIGHT JOIN)保留右表全部记录。
先看内连接的例子:
-- 查询学生姓名和他所在班级的名称(没有班级的学生不显示) SELECT student.name, class.class_name FROM student INNER JOIN class ON student.class_id = class.id;很多新手写 JOIN 会漏掉 ON 条件,这样又会产生笛卡尔积。我建议从一开始就养成习惯:ON 后面写的关联字段,一定要保证两边字段类型一致。我在实际开发中遇到过字符集不同导致关联查询巨慢的案例,后面排查半天才发现是表字段字符集一个 utf8mb4 一个 latin1,索引根本用不上。
再看左连接:
-- 查询所有学生的姓名和班级名,没分班的学生也要显示,班级名为 NULL SELECT student.name, class.class_name FROM student LEFT JOIN class ON student.class_id = class.id;这个语句的特点是:即使某个学生的 class_id 在班级表里找不到对应记录,学生信息仍然会保留,班级名称显示为 NULL。业务上常见的“查所有用户及其订单(没下过单的用户也要显示,订单列显示空)”就是这个场景。至于右连接,和左连接是对称的,实际工作中我几乎都通过调换表的写法来实现右连接的语义,因为 LEFT JOIN 更符合从左往右阅读的习惯。
4.3 子查询:把一条查询的结果当作另一条查询的输入
子查询简单说就是“套娃”:一条 SELECT 语句嵌套在另一条语句内部。它最常用的地方是在 WHERE 子句里把查询结果作为过滤条件。比如“找出成绩高于班级平均分的学生”:
SELECT name, score FROM student WHERE score > ( SELECT AVG(score) FROM student );这里外层查询对每一行都会执行一次子查询吗?并不会,MySQL 会先去执行子查询拿到一个标量值,再拿这个值去做外层全表比较。但如果子查询返回多行结果,比如“找出和 1 班学生同龄的所有学生”,就要配合 IN 来用:
SELECT name, age FROM student WHERE age IN ( SELECT age FROM student WHERE class_id = 1 );很多人从这里开始觉得 SQL 难了。我的建议是先把子查询单独跑一遍,确认它返回的结果符合预期,再嵌套到外面去。多层子查询嵌套时逻辑容易崩溃,如果发现某条语句越写越复杂,通常意味着该换 JOIN 重写了。子查询不是越多越好,能 JOIN 解决的就不要套娃。
5. 写数据也要讲规矩:INSERT、UPDATE、DELETE
讲完了查,接下来是改。这三个语句是 DML 的核心,但它们的危险程度完全不同:SELECT 最多查错数据,UPDATE 和 DELETE 要是写错条件,可能直接毁掉一整张表的数据。我在教学中最常强调的就是这两个语句的安全意识。
5.1 INSERT:写数据的三种姿势
最简单的就是指定字段名插入单行:
INSERT INTO student (name, gender, age, score, class_id, enroll_date) VALUES ('张三', 'M', 19, 87.50, 1, '2024-09-01');MySQL 还支持一条语句插入多行,用逗号分隔:
INSERT INTO student (name, gender, age, score, class_id, enroll_date) VALUES ('李四', 'M', 20, 91.00, 1, '2024-09-01'), ('王五', 'F', 18, 76.50, 2, '2024-09-02');第三种是通过查询结果来插入数据,比如把一张历史表的数据归档到新表:
INSERT INTO student_archive (name, score) SELECT name, score FROM student WHERE score < 60;这种INSERT INTO ... SELECT在数据迁移和备份场景里非常常用。但要注意字段数量和类型必须匹配,否则会报 ERROR 1136。我在第一天布置作业时,就遇到不少学员在这里报错,其实排查起来很简单:看 SELECT 返回了几列,INSERT 后边就写几列。
5.2 UPDATE 与 DELETE:没有 WHERE 就是事故
UPDATE 的语法长这样:
UPDATE student SET score = 90.5 WHERE id = 5;这里的WHERE id = 5是整条语句的生命线。如果漏掉,那么整张表所有学生的成绩都会变成 90.5。我知道这话听起来像废话,但真的有很多人在生产环境犯过这个错。我自己在早期工作里也踩过,从此养成一个习惯:执行 UPDATE 和 DELETE 之前,先用相同的 WHERE 条件跑一遍 SELECT 看看命中多少行,确认无误后再执行修改。
DELETE 和 TRUNCATE 的区别也要弄清楚。DELETE 是逐行删除,且支持加 WHERE 条件精准删除,删除后自增 ID 不会重置;TRUNCATE 是直接清空整张表,速度更快,但无法加条件,也不能回滚到具体行。如果只是想清空一张临时表,TRUNCATE 方便;如果要删特定记录,必须用 DELETE 并配好 WHERE。
5.3 事务:给你的写操作系上安全带
MySQL 的 InnoDB 引擎支持事务。事务的四大特性 ACID——原子性、一致性、隔离性、持久性——不必死记硬背,你只需要理解一个场景:转账从 A 账户扣钱,再往 B 账户加钱,这两步必须同时成功或者同时失败。如果扣钱成功、加钱失败,那这笔钱就凭空蒸发了。
最基础的事务写法是:
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; COMMIT; -- 如果执行过程中发现异常,用 ROLLBACK 回滚BEGIN 和 START TRANSACTION 等价。在实际开发中,尤其是执行多条 UPDATE 或 DELETE 语句时,我强烈建议用事务包起来。一旦某一步报错,你可以立即执行ROLLBACK让数据恢复到操作前的状态,这是最后一道防线。
6. 实操中的常见报错与避坑手册
这一节我专门整理一下 SQL-1 阶段最容易踩的坑。有些是我自己刚学时撞过的,有些是这几年带新人时反复看到的。把这些问题集中在一处,可以帮你少走很多弯路。
6.1 命令行里最常见的三种报错
ERROR 1064是语法错误。MySQL 会指出错误大概在第几行,但提示往往不精确。比如少写一个逗号或者少写一个右括号,它都可能报这一个错。我看到这个错误的第一反应永远是检查自己的标点符号,尤其是逗号和引号是不是写成了中文全角。
ERROR 1054是字段不存在。报错信息会直接告诉你哪个字段 Unknown column。排查思路是:先看该字段名是否拼写正确,再看表名是否写错。比如你对着学生表写SELECT nam FROM student,数据库不会智能纠正,只会报错。
ERROR 1146是表不存在。常见原因是表名大小写问题。MySQL 在 Linux 下表名是区分大小写的,在 Windows 下默认不区分。如果你在 Windows 上建的表叫 Student,拿到 Linux 生产环境用 student 去查,可能会直接报表不存在。
6.2 查询结果和预期不符的排查套路
如果 SQL 能跑通但结果不对,绝大多数是逻辑问题。我常用的排查套路是“逐步缩小范围”:先不加 WHERE 查全表看字段值,再加一个最简单的过滤条件,一步步定位问题。尤其是 JOIN 后数据变多或变少时,先分别查两张表确认数据,再检查 ON 的关联条件是否写反。
还有一个常见问题是使用别名导致歧义。多表查询时如果两张表都有同名字段,比如 student 表和 class 表都有 id,就必须写成student.id和class.id的形式,否则 MySQL 会报 Column 'id' in field list is ambiguous。给表起简短别名是个好习惯:FROM student s INNER JOIN class c ON s.class_id = c.id,语句一下就清爽多了。
另一个经典坑是条件里写了score = NULL,这个问题我在 2.3 提过,但几乎每期带人都会遇到。请牢记:判断 NULL 只能写IS NULL或IS NOT NULL,不能用等号,也不能用!=。
6.3 学习 SQL 第一课必须养成的五个好习惯
最后把这几天我最想让读者记住的操作习惯列出来:
- 每条 SQL 都以分号结尾,命令行里敲完回车不执行多半是漏了分号。
- 生产环境执行 UPDATE 和 DELETE 前,先用相同条件的 SELECT 验证影响行数。
- 学会看
SHOW WARNINGS的提示,MySQL 的警告有时比错误信息更有价值。 - 多表查询时养成显式 JOIN 的写法,不要用逗号连接表再到处放 WHERE 关联条件,后者在复杂查询里非常容易漏条件导致笛卡尔积。
- 语句要分层缩进,SELECT、FROM、WHERE、GROUP BY、ORDER BY 各占一行,这条看起来像格式洁癖,但实际排查问题时会救你命。
另外想特别提醒一句关于安全性的话:永远不要通过字符串拼接的方式把用户输入直接拼进 SQL 语句,尤其是在登录功能里。这不是危言耸听,而是 SQL 注入攻击最常见的入口。最典型的场景是用户输入一段精心构造的字符串,让你的 WHERE 条件永远为真,从而绕过账户密码校验绕过登录。这也是你学习 SQL 的第一天就应该建立的底线意识。
我个人的体会是,Day2 这节 SQL-1 是整个 MySQL 学习曲线里最“养手感”的一环。它不是靠看就能会的,需要你在命令行或者 Workbench 里把上面这些例子亲手敲一遍,再尝试改条件、换字段、变统计维度,直到哪条语句能预判出结果再执行。这样的练习做过几十次之后,你会发现自己看数据的眼光会发生一点变化:面对一个业务需求,脑子里浮现的不再是“该用什么函数”,而是“这张表和那张表怎么关联,条件在哪里过滤,最终要产出什么形态的结果”。这种思维方式,才是 SQL 真正给你的东西。
下一节我会接着讲 SQL-2 的内容,大概率会覆盖窗口函数、CASE WHEN 条件逻辑和更复杂的子查询嵌套。手里有 MySQL 环境的同学,建议今天先建好 student 表和 class 表,多跑几遍文中的查询和更新语句,把执行结果和你的预期对一对。坑早踩早好,等真正上了生产环境,再踩坑的成本就完全不一样了。