
很多人以为学 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 不是一个单一的命令而是由几类功能完全不同的语句组成的集合。我整理成一张表方便你按图索骥分类全称代表语句作用DQLData Query LanguageSELECT查询数据用的最多DMLData Manipulation LanguageINSERT、UPDATE、DELETE增删改记录DDLData Definition LanguageCREATE、ALTER、DROP定义库表结构DCLData Control LanguageGRANT、REVOKE控制权限TCLTransaction Control LanguageCOMMIT、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 表多跑几遍文中的查询和更新语句把执行结果和你的预期对一对。坑早踩早好等真正上了生产环境再踩坑的成本就完全不一样了。