
1. 写在前面的准备把环境先搞利索MySQL 的增删改查也就是 CRUDCreate、Read、Update、Delete是后端开发绝对绕不开的基本功。不管你是刚入行的新人还是写了几年业务代码的老手只要跟数据打交道每天写得最多的永远是这四类 SQL。不少人觉得增删改查太简单不值得专门花时间研究真到了线上出问题的时候才发现连一条 UPDATE 都写得战战兢兢。这一篇我先从最基础也最常用的部分讲起把新增、查询、更新、删除这四类操作彻底讲透配合一个贯穿全文的学生信息表案例让你看完之后能直接上手写业务 SQL。这一篇的内容定位是基础中的基础适合两类人一类是刚开始学 MySQL、连基本语法都还没理清的新手另一类是平时写 SQL 全靠百度、心里没底的开发者。我尽量把每一步的为什么也讲清楚而不只是丢给你一段能跑的命令。毕竟网上语法文档一大堆真正缺的是有人告诉你哪些地方容易踩坑、哪些写法性能会翻车。在开始写增删改查之前我先把准备工作讲清楚包括 MySQL 怎么装、用哪个客户端工具连库方便、连接常见问题怎么排查。这些内容看似跟 CRUD 没直接关系但环境不顺溜后面全是坑。1.1 安装与启动版本选择和服务启停MySQL 安装这块网上教程铺天盖地但版本选择上很多人栽过跟头。我这里直接给出结论新项目优先选择 MySQL 8.0 及以上版本8.0 在性能、安全、窗口函数、公共表表达式等方面比 5.7 提升明显而且官方对 5.7 的终身支持已经结束继续用老版本等于把风险留给线上。当然如果你维护的还是 5.7 的老项目也不急着马上迁移但新库别再用 5.7 了。安装方式取决于你的操作系统。如果是 Windows直接去 MySQL 官网下载安装包选 ZIP 压缩包或者 MSI 安装包都行。MSI 图形化安装更省事一路 Next中间会让你设置 root 密码建议选一个强密码并牢记后面所有连接都要用。如果是 Linux常见的是用 rpm 包离线安装或者通过发行版的软件仓库安装再或者用 Docker 容器跑一个实例。Docker 方式最干净一条命令就能起一个 MySQL 8.0docker run --name mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORDyour_password -d mysql:8.0用 Docker 的好处是隔离干净不会把宿主机搞乱而且删除重来都很方便。缺点是要额外维护容器数据卷别把数据裸奔存在容器里一定要挂载数据目录出去。安装完之后第一步是确认服务起来了。Linux 上常见命令是systemctl status mysqld或service mysql status如果启动失败最快的排查方式是看日志tail -f /var/log/mysql/error.log。很多新手卡在这一步报错原因五花八门有的是数据目录权限不对有的是端口被占用有的是配置文件里 socket 路径和实际不匹配。别慌一个个看日志就能定位日志里会明确告诉你原因。1.2 客户端工具选择与连接库前的检查服务跑起来之后你还需要一个工具来执行 SQL。我推荐三个按使用场景选命令行客户端mysql 命令连接数据库最直接的方式也是排查问题时最可靠的方式。适合服务器上操作脚本化执行 SQL 时用。MySQL Workbench官方图形化工具免费支持 ER 图、查询、管理。适合学习和日常管理。Navicat / DBeaver第三方客户端界面更友好连接配置更直观适合日常开发查询。Navicat 收费但体验好DBeaver 开源免费功能也不弱。不管用哪个工具连接 MySQL 都需要四个信息主机地址host、端口默认 3306、用户名、密码。本机连接通常 host 填127.0.0.1或localhost但这里有个经典坑localhost在 Unix 系统上默认走 socket 方式连接127.0.0.1走 TCP 连接。如果 MySQL 服务端配置的 socket 文件路径不对或者你用localhost连但服务没监听在默认 socket 上就会遇到那个很出名的报错ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock。这个报错我在第五节单独讲。连接前我还建议执行一条检查命令确认当前连接的基本信息SELECT VERSION(), CURRENT_USER(), DATABASE();如果你能看到版本号、当前用户和当前数据库说明连接成功了。查出来DATABASE()是 NULL 也不用担心这只是代表当前没有选中数据库执行USE 数据库名;切换就行。2. 设计一张测试表CRUD 之前先做表结构任何增删改查都是作用在具体的表和字段上的。在讲语法之前我先建一张贯穿全文的学生信息表后面所有 INSERT、SELECT、UPDATE、DELETE 示例都用这张表这样上下文是连贯的你也能看到同样的数据在不同操作之间的变化过程。2.1 表结构设计思路与原因设计这张表的时候我特意考虑了三个方面一是字段类型要足够典型能覆盖日常开发中的常见场景二是要有默认值、唯一约束、索引这些元素方便后面讲解 CRUD 时的各种细节三是表结构要简单不引入外键和复杂关联避免分散注意力。表结构如下CREATE TABLE student ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键ID, stu_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 性别 0未知 1男 2女, birth_date DATE COMMENT 出生日期, score DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT 平均成绩, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_stu_no (stu_no), KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表;几个关键设计点我说一下思路。主键id用INT UNSIGNED自增这个是最常见的方案无符号整数能扩大取值范围自增主键在 InnoDB 引擎下性能好、插入顺序友好。学号stu_no加了唯一索引因为学号天然不允许重复这不仅保证了数据正确性也为以后按学号查询提供了索引加速。gender用TINYINT存枚举值而不是直接存字符串省空间、查询快但代价是业务代码里要知道 0、1、2 分别代表什么我会写清楚注释。score用DECIMAL(5,2)表示最大 999.99 的数值精确小数不能用 FLOAT 或 DOUBLE因为二进制浮点数在比较和累加时会有精度误差这是新手容易忽略的点。create_time和update_time是每一个业务表都应该有的审计字段。DEFAULT CURRENT_TIMESTAMP让插入时自动记录时间ON UPDATE CURRENT_TIMESTAMP让更新时自动刷新修改时间。这两个字段能省掉大量业务代码而且排查数据问题时作用极大。字符集统一用utf8mb4这是 MySQL 8.0 的默认字符集支持完整的 Unicode 字符包括低版本的utf8不支持的 emoji 和生僻汉字。很多老项目出现插入特殊字符变成问号的问题多半就是字符集没设为 utf8mb4。2.2 执行建表语句并验证表结构建表语句在命令行或者图形客户端里直接执行即可。执行成功后用SHOW CREATE TABLE student;查看表的完整定义确认没有语法问题。也可以用DESC student;查看表的字段结构返回结果长这样--------------------------------------------------------------------------------------------------- | Field | Type | Null | Key | Default | Extra | --------------------------------------------------------------------------------------------------- | id | int unsigned | NO | PRI | NULL | auto_increment | | stu_no | varchar(20) | NO | UNI | NULL | | | name | varchar(50) | NO | MUL | NULL | | | gender | tinyint | NO | | 0 | | | birth_date | date | YES | | NULL | | | score | decimal(5,2) | NO | | 0.00 | | | create_time | datetime | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | | update_time | datetime | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | ---------------------------------------------------------------------------------------------------看到这个结构之后就可以放心开始增删改查了。接下来的所有操作都在student表上进行如果你是用命令行连接的别忘记先USE 你的数据库名;。注意KEY idx_name (name)表示给name字段建了一个普通索引但索引不等于唯一约束普通索引允许重复值。这对后面的查询性能有好处因为按姓名搜索的场景很常见。3. 新增数据INSERT 的完整姿势和常见坑新增数据是所有业务操作的起点。没有数据后面查询、更新、删除都是空谈。这一节我把 INSERT 从最基础的语法讲到批量插入的细节并把插入时的编码问题、日期格式问题、默认值使用这些高频踩坑点都讲透。3.1 单条插入的三种写法与语法要点INSERT 最基本的语法是列名和值一一对应。我推荐最严谨的写法显式列出插入的列INSERT INTO student (stu_no, name, gender, birth_date, score) VALUES (S001, 张伟, 1, 2005-03-15, 88.50);这种写法最大的好处是字段顺序随便调整都不受影响只要列名列表和VALUES里的值一一对应就行。比如我可以把顺序调成(name, stu_no, score, birth_date, gender)照样能正确插入。更重要的是当表结构后续增加字段时这种写法不会因为列数变化而出错。第二种写法省略列名INSERT INTO student VALUES (1, S002, 李娜, 2, 2004-11-02, 91.00, NOW(), NOW());这种写法要求把表里所有字段都按顺序写上一个都不能少顺序不能错。它的优点是简洁但代价是可读性差、易错。一旦字段顺序记错数据就串位了。我不建议在业务代码里用这种写法手写 SQL 调试时临时用一下倒无所谓。第三种写法是指定部分列让其他列走默认值INSERT INTO student (stu_no, name) VALUES (S003, 王强);执行之后gender会取默认值 0score取默认值 0.00create_time和update_time自动填当前时间id自动生成。这种写法在业务中非常常见比如注册功能只需要用户名和密码其他字段等用户后续补全。插入之后怎么确认成功了SELECT查一下就知道。为了后续示例方便我把准备好的测试数据一次性插入INSERT INTO student (stu_no, name, gender, birth_date, score) VALUES (S001, 张伟, 1, 2005-03-15, 88.50), (S002, 李娜, 2, 2004-11-02, 91.00), (S003, 王强, 1, 2005-07-20, 76.00), (S004, 赵敏, 2, 2003-01-08, 95.50), (S005, 孙磊, 1, 2005-09-30, 82.00);这就是批量插入一条语句同时插入多行效率远高于逐条执行五次单行 INSERT。批量插入时注意值列表数量要和前面的列数一致每行括号之间用逗号分隔。3.2 插入数据时的编码、日期与默认值问题插入数据看起来简单但实际开发中经常遇到三类问题。第一类是中文乱码或 emoji 写入失败。我建表时用了 utf8mb4这还不够连接层也必须用 utf8mb4。命令行连接时在执行插入之前先加一句SET NAMES utf8mb4;这句的作用是告诉服务器当前客户端发送过来的文本是用 utf8mb4 编码的服务器不会用错误编码解析。如果你用 Navicat 或 DBeaver 这类图形客户端一般在连接属性里设置编码即可。如果忘记设置插入的中文可能变成???或者报Incorrect string value错误。第二类是日期字符串的格式问题。birth_date是 DATE 类型插入时传字符串需要符合 MySQL 的日期格式最标准的是YYYY-MM-DD比如2005-03-15。如果你手里的日期数据是2005/03/15这种斜杠格式MySQL 在大多数情况下也会自动转换但20050315这种无分隔符格式则不一定被接受。最稳妥的方式是在插入前先把日期字符串规范成YYYY-MM-DD格式。如果你是从外部数据文件导入而且格式很乱可以先用STR_TO_DATE函数做转换这个我放到第五节问题排查里单独讲。第三类是自增主键的行为。自增列在插入时不需要填值即使你填了 0 或 NULLMySQL 也会自动生成新值。但如果你显式插入了一个具体的 id 值比如id 10那么下一次自增会从 11 开始。这个行为在表被清空之后仍然有效也就是说 DELETE 全部数据之后再用默认方式插入的新记录 id 并不会从 1 重新开始而是接着之前的最大值继续。这个概念后面讲 DELETE 的时候还会提到。经验INSERT 语句有一个高频面试考点——插入多条数据时的自增 ID 分配。如果事务回滚了已经申请到的自增 ID 不会被回收会出现 ID 空洞。这是正常现象不要尝试用最大 ID 加 1 的方式去推测下一个 ID。4. 查询数据SELECT 是每天都在写的核心操作查询是增删改查中使用频率最高、也是变化最多的一种操作。一个业务系统里SELECT 语句的数量占比通常在 80% 以上。这一节我从最简单的全表查询讲起逐步覆盖条件过滤、排序、分页、模糊匹配这些都是必须熟练掌握的基本功。4.1 基础查询与 WHERE 条件过滤最简单的查询是查看表里所有数据SELECT * FROM student;*表示所有列。日常开发里我建议少用*而是显式列出需要的列原因有两个一是只取需要的列能减少网络传输量和内存占用二是不依赖表结构顺序后续表加了字段SELECT *的结果集结构会变容易导致程序解析出错。显式指定列的查询SELECT stu_no, name, gender, score FROM student;如果觉得表名字段名太长可以给它们起别名SELECT s.stu_no AS 学号, s.name AS 姓名, s.score AS 成绩 FROM student AS s;lihat: 这里用了单表别名s特别是多表查询的时候别名能让 SQL 简洁很多。别名的AS关键字可以省略写成s.stu_no 学号但为了可读性我建议保留。WHERE 条件过滤是查询的核心也是初学者最容易理解偏差的地方。WHERE 子句的作用是逐行判断条件是否成立只返回满足条件的行。常用运算符包括SELECT * FROM student WHERE score 90; SELECT * FROM student WHERE gender 2 AND score 85; SELECT * FROM student WHERE name 张伟 OR name 李娜; SELECT * FROM student WHERE score BETWEEN 80 AND 90; SELECT * FROM student WHERE name IN (张伟, 李娜, 王强); SELECT * FROM student WHERE birth_date IS NULL;注意IS NULL的判断写法不能用 NULL因为 NULL 表示未知任何与 NULL 的直接比较结果都是 NULL假而不是真。这一点我见过不少人写错WHERE birth_date NULL永远查不出数据。4.2 排序、分页与模糊查询实战排序使用 ORDER BY默认升序。按成绩降序排列SELECT stu_no, name, score FROM student ORDER BY score DESC;多字段排序时先按第一个字段排相同再按第二个字段排SELECT stu_no, name, gender, score FROM student ORDER BY gender ASC, score DESC;ASC是升序可以省略DESC是降序不能省略。排序字段可以是表中字段也可以是表达式或别名比如按显示名排序SELECT stu_no, name, score AS 成绩 FROM student ORDER BY 成绩 DESC;分页是列表类业务的必备技能。MySQL 用LIMIT来实现语法是LIMIT 偏移量, 行数。偏移量从 0 开始查询第 1 页每页 5 条就是SELECT stu_no, name, score FROM student ORDER BY score DESC LIMIT 0, 5;查询第 2 页时偏移量变为 5LIMIT 5, 5。通用的计算公式是偏移量 (页码 - 1) * 每页行数。这里有个好习惯使用 LIMIT 分页时一定要同时使用 ORDER BY否则返回结果的顺序不固定分页会出现数据重复或缺失。模糊查询用 LIKE配合%通配符匹配任意多个字符_匹配单个字符SELECT stu_no, name FROM student WHERE name LIKE 张%; SELECT stu_no, name FROM student WHERE name LIKE _伟;第一条查所有姓张的学生第二条查名字第二个字是伟的学生比如张伟。模糊查询在小数据量下没问题但一旦表数据量到了百万级别LIKE %关键字%这种头部带通配符的写法无法走索引只能全表扫描性能会急剧下降这个后面再展开。4.3 聚合查询与运算表达式的基础用途虽然聚合函数大概率会在增删改查二里详细讲但既然这一篇说了查询我先把最常用的几个聚合函数点一下因为它们在业务报表里太常见了。SELECT COUNT(*) AS 学生总数, MAX(score) AS 最高分, MIN(score) AS 最低分, AVG(score) AS 平均分, SUM(score) AS 总分 FROM student;COUNT 统计行数MAX/MIN 取最大最小值AVG 求平均SUM 求和。这些函数忽略 NULL 值COUNT(*) 除外它统计的是行数。聚合函数的本质是没有 GROUP BY 时把整张表当作一组来计算。如果希望按性别分别统计就要引入 GROUP BY 了——这个我在下一篇重点展开这里先记住有这个用法。再补充一个关于算术表达式的点。热搜里有一条mysql中int5这个其实是运算符的用法。比如想给每个学生成绩统一加上平时分 5 分后查询SELECT stu_no, name, score 5 AS 加平时分后的成绩 FROM student;字段可以直接参与算术运算加减乘除、取模都可以。但要注意运算不影响存储的真实数据只是影响查询结果集的展示。如果想真正改数据那是 UPDATE 的事。5. 更新与删除数据UPDATE 和 DELETE 的安全红线更新和删除是四个操作里风险最高的两个因为一旦 WHERE 条件写错影响的就是整张表的数据。这一节我先把正确语法讲清楚再重点讲防误操作的方法论这是所有数据库开发者的必修课。5.1 UPDATE 语法、批量更新与常见误操作UPDATE 的基本语法是UPDATE 表名 SET 列 新值 WHERE 条件。把某个学生的成绩改掉UPDATE student SET score 90.00 WHERE stu_no S001;执行后会有返回信息显示Rows matched: 1这个数字代表匹配到的行数。MySQL 默认如果更新的值和原值一样会影响 0 行但匹配到的行数仍会显示 1这是正常现象。一次更新多个字段用逗号分隔UPDATE student SET score 92.00, gender 2 WHERE stu_no S001;更新还可以结合表达式比如给所有男生成绩加 5 分UPDATE student SET score score 5 WHERE gender 1;这个写法非常典型注意score score 5右边的score是更新前的值MySQL 会先用旧值计算完再赋值给新值不用担心循环引用问题。关于 UPDATE最核心的提醒是WHERE 条件一定不能省。如果不带 WHEREMySQL 会更新表里所有行UPDATE student SET score 0;这条语句会把所有学生的成绩清为零。在开发环境做测试可能无伤大雅但在生产环境执行一次轻则数据被改错重则直接事故。我在实际带团队的时候定了一条铁的规矩上线执行的 UPDATE 和 DELETE 语句必须经过 WHERE 条件审查禁止裸 UPDATE 和裸 DELETE。另一个常见的错误是 WHERE 条件写错但符合语法。比如要更新学号为 S001 的学生的成绩结果条件写成了WHERE name 张伟如果恰好有两个学生都叫张伟就会把两条记录都改了。所以在更新之前建议先执行同条件的 SELECT确认影响范围SELECT * FROM student WHERE name 张伟;先查后改这个习惯能帮你挡掉一大半误操作。在事务里操作更稳妥这个我马上讲。5.2 DELETE 语法、差异对比与事务保护DELETE 的语法和 UPDATE 类似DELETE FROM student WHERE stu_no S005;执行成功后会显示删除的行数。不带 WHERE 的 DELETE 会清空整张表DELETE FROM student;和 UPDATE 一样删除前一定要先 SELECT 确认范围。删除整张表时DELETE FROM和TRUNCATE TABLE有本质区别我整理了一个对比表对比项DELETE FROMTRUNCATE TABLE条件删除支持 WHERE不支持只能全表清空速度逐行删除慢直接释放表空间极快事务支持支持可回滚不支持执行后立即生效自增 ID不清零继续累加清零重新从 1 开始触发器触发不触发实际开发中清空表的场景并不多而且一旦执行了不带 WHERE 的 DELETE 或 TRUNCATE后果都很严重。TRUNCATE 因为不支持事务回滚风险更高执行前一定要反复确认是不是真的要把整张表清空。事务保护是数据库安全的重灾区也是救命稻草。MySQL 的 InnoDB 引擎支持事务核心命令是BEGIN、COMMIT、ROLLBACK。比如要删除一批数据可以这样操作BEGIN; DELETE FROM student WHERE score 80; -- 先不提交检查结果 SELECT COUNT(*) FROM student WHERE score 80; -- 确认无误后提交 COMMIT; -- 如果发现删错了回滚 ROLLBACK;在事务里执行 UPDATE 或 DELETE提交前其他会话是看不到变更的而且随时可以回滚。我在生产环境处理敏感数据时永远先开事务多查几遍再提交宁可多花一分钟也不冒一夜返工的风险。注意使用事务需要表引擎是 InnoDB。MyISAM 引擎不支持事务这也是为什么新表一律推荐 InnoDB 的原因之一。6. 实战问题排查连接错误、类型转换、SQL 性能基础写 SQL 这件事光会语法远远不够还得能处理各种运行时报错和性能隐患。这一节我把常见问题整理成速查表并挑几个高频的坑展开讲透。6.1 常见连接与执行错误速查表我在指导新人过程中收集了一批出现频率最高的报错和信息整理如下错误信息或现象常见原因处理思路ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock服务未启动或 socket 路径不匹配先确认服务状态再确认配置文件里的 socket 路径ERROR 1045 (28000): Access denied for user用户名或密码错误检查密码或让 DBA 重置权限ERROR 1146: Table doesnt exist表名打错或没有选中数据库确认库名表名确认当前 use 的库ERROR 1366: Incorrect string value字符集不匹配文本不在目标字符集内设置SET NAMES utf8mb4检查连接字符集ERROR 1064: You have an error in your SQL syntaxSQL 语法写错对照语法检查关键字、逗号、引号中文变成 ???连接层字符集和服务端不一致统一 utf8mb4插入很慢没走索引或插入频繁提交检查索引考虑批量插入和事务合并查询很慢缺少索引或 SQL 写法导致全表扫描用 EXPLAIN 分析执行计划执行报错并不可怕关键是看错误码和错误信息。MySQL 的错误码已经非常规范照着错误的提示信息逐条排查基本都能解决。6.2 典型案例一ERROR 2002 socket 无法连接的排查过程这个错误在热搜里出现了完整版ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock。它的本质是客户端尝试通过 Unix socket 连接 MySQL 服务时发现 socket 文件不存在或无法访问。排查步骤我按顺序建议如下第一步确认服务是否真的在运行。在我接触的案例里超过一半是服务挂了没起来。Linux 上执行systemctl status mysqld如果显示 inactivedead那就启动服务systemctl start mysqld。第二步服务已经在运行但 socket 路径不对。查看 MySQL 配置文件/etc/my.cnf或/etc/mysql/mysql.conf.d/mysqld.cnf找到[mysqld]段下的socket配置项确认路径。有的版本 socket 路径是/var/run/mysqld/mysqld.sock但客户端默认连的是/tmp/mysql.sock两者不一致就会报错。解决办法是在客户端连接时显式指定 socket 路径mysql -uroot -p --socket/var/run/mysqld/mysqld.sock或者干脆使用 TCP 方式连接指定 host 和 portmysql -uroot -p -h127.0.0.1 -P3306注意这里用127.0.0.1而不用localhost这样会走 TCP 而不是 socket可以绕开 socket 路径不一致的问题。第三步检查 socket 文件的权限。即使路径对了如果 MySQL 运行用户没有权限创建或访问 socket 文件同样会报错。查看/var/run/mysqld目录的所有者和权限确保属主是 mysql 用户。这个报错还有一个变体是在你用 PHP PDO 连接时出现热搜里有一条call stack in connection.php line 528 at pdo-__construct(mysql:host127.0...本质也是 DSN 配置里 host 用了 localhost 或 socket 路径不对。DSN 中直接配置 host 为127.0.0.1或者显式指定unix_socket可以绕开这个坑。6.3 典型案例二日期字符串转换与类型处理业务系统经常需要把外部传入的日期字符串写入 DATE 或 DATETIME 字段比如 CSV 导入、接口对接等场景。如果对方给的字符串格式是2024年3月15日或2024/03/15这种非标准格式MySQL 可能无法自动识别插入时会报错或插入 NULL。处理方法是用STR_TO_DATE函数做显式转换。它的语法是STR_TO_DATE(字符串, 格式)SELECT STR_TO_DATE(2024/03/15, %Y/%m/%d); SELECT STR_TO_DATE(2024年3月15日, %Y年%m月%d日); SELECT STR_TO_DATE(20240315, %Y%m%d);常用格式符有%Y四位年份、%m两位月份、%d两位日、%H小时、%i分钟、%s秒。转换失败时返回 NULL所以生产环境做数据导入时一定要先跑一条 SELECT 验证转换结果再执行 INSERT。反向转换同样常见把日期类型转成指定格式的字符串用DATE_FORMATSELECT DATE_FORMAT(birth_date, %Y-%m-%d) FROM student;日期转换函数是增删改查之外的必备补充技能因为业务系统里字符串和日期类型的转换无处不在学会这两组函数能省掉大量在应用层写代码的功夫。6.4 基础性能意识EXPLAIN 和索引意识增删改查虽然基础但性能问题往往在看似正常的 SQL 中埋下。我在这里先提两个基本工具详细的索引调优留给后续专题文章。第一个是 EXPLAIN。在 SELECT 语句前面加上 EXPLAIN不会真正执行查询而是输出执行计划告诉你这条 SQL 是怎么查的EXPLAIN SELECT * FROM student WHERE stu_no S001;重点看type列和key列。如果type显示ALL说明是全表扫描一旦表数据量大就会很慢如果显示的是const或ref说明走了索引性能通常是好的。key列会显示实际用到的索引名我们的表里uk_stu_no是唯索引所以按学号查询应该走到const级别。第二个是索引意识。写 WHERE 条件时尽量让字段本身不要被函数包裹。比如WHERE DATE_FORMAT(create_time, %Y-%m-%d) 2024-01-01这种写法会导致create_time上的索引失效因为每个值都要先被函数处理再比较。改成范围查询WHERE create_time 2024-01-01 AND create_time 2024-01-02就能走索引。这个细节在数据量大的时候性能差一个量级。据我观察很多开发者的 SQL 性能问题不是语法不会而是缺少查询能不能走索引这根弦。在写每一条查询时问自己一句这个条件字段有没有索引能不能避开函数包裹养成这个习惯你的 SQL 会明显比别人快。这一篇的增删改查基础操作到这里就全部讲完了。我最后分享几条自己写 SQL 坚持了很多年的习惯写 UPDATE 和 DELETE 之前先跑同条件 SELECT敏感操作必须在事务里执行确认无误再提交字符集一律 utf8mb4日期比较用范围查询而不是函数包裹字段列。这几条习惯帮我挡掉了无数次事故也希望对你也有用。下一篇我会接着讲条件查询的进阶用法包括 GROUP BY 分组统计、HAVING 过滤、JOIN 多表连接和子查询那些才是真正拉开 SQL 水平差距的地方。