MySQL入门绕不开的坎,就是增删查改这四个动作,也就是常说的CRUD。很多教程把增删查拆得七零八落,讲插入的只讲插入,讲查询的只讲查询,读者看完感觉自己什么都见过,可真到工作里要动手建表、写查询、改数据的时候,还是不知道从哪里下手。这篇内容就是为了解决这个问题:我拿一张学生信息表做贯穿案例,把MySQL基础操作中的增删查改从语法到实战一路走通,每一步都告诉你为什么这么写、不这么做会踩什么坑。适合刚接触数据库的同学,也适合想把自己零碎的MySQL知识系统过一遍的人。
1. 动手前的准备工作
1.1 环境与连接工具
先别急着敲SQL,一上来就操作很容易被各种小问题劝退。MySQL本身要先装好,装完以后你得有个趁手的连接工具。
命令行是默认选项,也是最稳妥的。Windows下打开cmd,Linux/Mac下打开终端,输入类似这样的命令就能连上本地MySQL:
mysql -uroot -p输入密码后,看到mysql>提示符就说明进来了。命令行适合练基本功,所有SQL都能跑,但缺点也很明显,查询结果多了以后排版不够直观。
图形化工具我建议新手至少准备一个。最常用的是Navicat和DBeaver,另外MySQL官方也提供免费的MySQL Workbench。这三者选哪个都行,只要能把数据库连上、能看表结构、能执行SQL就可以。DBeaver是免费的跨平台工具,社区版就够用;Navicat功能强大,但很多功能需要付费授权,而且网上流传的所谓“破解版”既不合规,也可能带有安全风险,我自己从来不用,也建议你使用正版或免费工具。
连接工具本质上只是个壳,它把你要执行的SQL发给MySQL服务器,再把结果渲染出来。所以不管用哪个工具,核心还是SQL本身。
1.2 建库建表:数据类型先选对
操作数据的顺序一般是:先建数据库,再建表,最后才能往里塞数据。创建一个数据库很简单:
CREATE DATABASE school DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;这里一定要指定utf8mb4字符集。MySQL老版本默认的utf8其实是utf8mb3,它存不了emoji和部分生僻字,报错的时候往往只给你一个冷冰冰的“Incorrect string value”,排查半天才发现是字符集问题。utf8mb4是utf8的超集,能兼容更多字符,现在新建库表直接用utf8mb4就行了。
建表是增删查改之前最关键的一步,表结构定得不好,后面CRUD全都别扭。拿学生表来说,基础字段设计如下:
CREATE TABLE student ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', name VARCHAR(50) NOT NULL COMMENT '姓名', gender ENUM('male', 'female') DEFAULT NULL COMMENT '性别', age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄', class_name VARCHAR(50) DEFAULT NULL COMMENT '班级', score DECIMAL(5,2) DEFAULT 0.00 COMMENT '总分', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表';几个字段类型的选择思路要讲清楚。id用INT UNSIGNED,主键一般不用负数,无符号整数可以让正数范围翻倍;age用TINYINT UNSIGNED,因为人的年龄撑死一百多,TINYINT范围是0~255,完全够用,还省空间;score用DECIMAL(5,2),因为浮点数的精度在MySQL里不靠谱,存金额、分数这类需要精确计算的数值,一定要用定点数DECIMAL;created_at用DATETIME,配合默认值CURRENT_TIMESTAMP,插入数据的时候会自动记录当前时间。
ENGINE=InnoDB也必须写上。InnoDB是MySQL默认引擎,支持事务、行级锁、外键,生产环境基本都用它。MyISAM虽然查询快一点,但不支持事务,崩溃后恢复能力也差,除非你有特殊理由,否则一律InnoDB。
2. INSERT:把数据装进口袋
2.1 单条插入与批量插入
INSERT做的事就是给表里加数据。最简单的单条插入写法:
INSERT INTO student (name, gender, age, class_name, score) VALUES ('张三', 'male', 18, '高一(3)班', 580.5);表名后面括号里写字段名,VALUES后面写对应值。这里有个习惯问题:我见不少人写INSERT时省略字段列表,直接写INSERT INTO student VALUES (...),这种写法要求值的顺序必须和表结构完全一致,一旦表加了字段或者调整了顺序,这条SQL就废了。生产中表结构隔三差五会变,所以强烈建议每次都用显式字段列表。
一次插入多条数据时有更高效的做法,把多个值组用逗号连起来:
INSERT INTO student (name, gender, age, class_name, score) VALUES ('李四', 'female', 17, '高一(2)班', 592.0), ('王五', 'male', 18, '高一(3)班', 561.0), ('赵六', 'female', 17, '高一(1)班', 598.5);批量插入不仅是写法更紧凑,更是性能上的选择。每条INSERT单独执行,MySQL每次都要重新解析SQL、做权限检查、记录日志,批量插入可以把这些开销摊薄到每条记录上。几千条数据场景下,这个差别就已经很明显了。我实测往一张10万行级别的表里灌数据,单条逐行插入可能要几分钟,改成一次插入500条,速度能提升一个数量级。
2.2 插入时的常见坑
第一,字段和值的数量必须严格对应。少写一个值会报Column count doesn't match value count。这属于低级错误,但越忙的时候越容易犯。
第二,要注意默认值的行为。上面表里的gender、class_name是允许为NULL的,created_at有默认值。所以可以只插入部分字段:
INSERT INTO student (name, age) VALUES ('周七', 18);这样gender、class_name自动是NULL,score会用默认值0.00,created_at自动填充当前时间。反过来,如果你连id都想自己指定,也不是不行,id字段因为是AUTO_INCREMENT,你手动给个值也可以,但除非有特殊场景,我建议永远不要手动指定自增主键,让MySQL自己分配就好。
第三,字符串里的单引号需要转义。比如学生叫“O'Brien”,直接写会语法错误,要写成:
INSERT INTO student (name) VALUES ('O\'Brien');另外,如果需要插入的数据在唯一键上冲突时想要“有则更新、无则插入”,可以用ON DUPLICATE KEY UPDATE:
INSERT INTO student (id, name, age) VALUES (1, '张三', 19) ON DUPLICATE KEY UPDATE name = VALUES(name), age = VALUES(age);这个方法在批量同步数据时非常好用,但注意它依赖唯一索引或主键,没有索引限制的时候MySQL不知道“重复”指的是什么。
3. SELECT:把数据翻出来看
3.1 基础查询和条件过滤
查询是CRUD里最常写、也最值得花时间学的一部分。最简单的全表查询:
SELECT * FROM student;*表示所有字段,开发调试的时候很方便,但生产环境尽量少用SELECT *,尤其是表字段多、数据量大的时候。原因有两个:一是多查了很多用不到的字段,白白增加网络传输和内存消耗;二是如果哪天表结构加了字段,代码里拿结果集的顺序可能就乱了。所以查什么字段就写什么字段:
SELECT id, name, age FROM student;条件过滤是查询的灵魂。WHERE子句用来指定过滤条件,比如查所有18岁的学生:
SELECT name, age FROM student WHERE age = 18;条件里可以用各种运算符,=、>、<、>=、<=、!=,以及逻辑运算符AND、OR、NOT。比如查高一(3)班且分数大于560的:
SELECT name, class_name, score FROM student WHERE class_name = '高一(3)班' AND score > 560;模糊查询用LIKE。比如查所有姓“张”的学生:
SELECT name FROM student WHERE name LIKE '张%';%是通配符,代表任意长度的任意字符;_代表单个字符。这里有个性能问题:LIKE '%张'这样以通配符开头的写法,会导致索引失效,全表扫描。数据量小无所谓,数据量大的时候一定要避免。
范围查询可以这样写:
-- 用 AND 写法 SELECT name, age FROM student WHERE age >= 16 AND age <= 18; -- 用 BETWEEN 写法,效果一样 SELECT name, age FROM student WHERE age BETWEEN 16 AND 18;3.2 排序、去重与分页
查询结果默认是按物理存储顺序返回的,大多数时候不是你想要的顺序。用ORDER BY控制排序:
-- 按分数从高到低 SELECT name, score FROM student ORDER BY score DESC; -- 按年龄从小到大,年龄相同时按分数从高到低 SELECT name, age, score FROM student ORDER BY age ASC, score DESC;DESC表示降序,ASC表示升序,默认是ASC。多字段排序时,按写的顺序逐级比较,这也是考试里常考的知识点。
去重用DISTINCT。比如想看看学生都分布在哪几个班级:
SELECT DISTINCT class_name FROM student;注意,SELECT DISTINCT name, class_name去重的是(name, class_name)的组合,不是只对第一个字段去重,这个容易理解错。
分页是生产环境非常高频的需求,用LIMIT:
-- 跳过前0条,取10条,即第1页 SELECT name, score FROM student ORDER BY score DESC LIMIT 0, 10; -- 跳过前10条,取10条,即第2页 SELECT name, score FROM student ORDER BY score DESC LIMIT 10, 10;LIMIT offset, count的写法里,offset是从第几条开始跳,count是取多少条。记住分页公式:第N页的数据,offset = (N-1) * pageSize。这里注意,深分页是个性能杀手,LIMIT 1000000, 10需要MySQL先数出前面100万条再丢弃,非常慢。解决思路通常是先用索引定位到起始位置再往后取,或者记录上一页最后一条数据的id作为下一页起点。
3.3 聚合统计与GROUP BY
光把数据列出来还不够,业务上经常要“统计”。聚合函数就是干这个的,常见的有COUNT、SUM、AVG、MAX、MIN。比如:
-- 学生总数 SELECT COUNT(*) FROM student; -- 分数平均值 SELECT AVG(score) FROM student; -- 最高分和最低分 SELECT MAX(score), MIN(score) FROM student;把统计按组拆开,就得用GROUP BY。比如统计每个班有多少人、平均分多少:
SELECT class_name, COUNT(*) AS cnt, AVG(score) AS avg_score FROM student GROUP BY class_name;这里AS是给查询结果字段起别名,显示的时候更好看懂。GROUP BY之后如果想过滤组,不能再用WHERE,得用HAVING。比如只要平均分大于580的班级:
SELECT class_name, AVG(score) AS avg_score FROM student GROUP BY class_name HAVING avg_score > 580;WHERE和HAVING的区别很多新手搞混:WHERE是在分组之前对每一行做过滤,HAVING是在分组之后对每一组做过滤。这个顺序不能反,否则语义就错了。
3.4 多表查询的入门思路
基础操作阶段可以只操作单表,但实际开发中几乎没有只查一张表的业务。拿学生表来说,如果每个学生还有一张成绩明细表,查询“学生姓名+他的各科成绩”,就需要把两张表连起来。JOIN就是干这个的:
SELECT s.name, g.subject, g.score FROM student s INNER JOIN grade g ON s.id = g.student_id;JOIN的核心逻辑是:把左表的每一行拿出来,跟右表的每一行做匹配,匹配条件是ON后面写的等式。INNER JOIN只保留两边都匹配得上的行,LEFT JOIN保留左表所有行,匹配不上则右表字段补NULL。多表查询的细节能写好几篇文章,这里先明白“JOIN是把两张表按关联条件拼成一张大表”这个思路就够了。
4. UPDATE:修改已有数据
4.1 基本更新语法
UPDATE用来修改表中的数据。基本语法:
UPDATE student SET age = 19 WHERE name = '张三';从逻辑上讲,UPDATE做的事是:找到符合WHERE条件的行,把这些行的age字段改成19。这里最重要的就是WHERE条件。如果不写WHERE,MySQL会把整张表的所有行都更新:
-- 危险!把所有人的年龄都改成19 UPDATE student SET age = 19;很多初学者都在这里栽过跟头。MySQL的默认安全模式下,不带WHERE或WHERE里没有使用索引的UPDATE会直接报错:You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column。这其实是保护你的机制,它能拦下“忘记写WHERE”的低级失误。所以我建议新手把安全模式开着,别急着关掉它。
4.2 更新多个字段和批量更新技巧
更新多个字段用逗号分隔:
UPDATE student SET age = 19, score = 590.5 WHERE id = 1;批量更新不同记录的不同值,有一种常见做法是用CASE WHEN。比如要把“高一(1)班”的分数统一加5分,“高一(2)班”的分数统一加10分:
UPDATE student SET score = CASE class_name WHEN '高一(1)班' THEN score + 5 WHEN '高一(2)班' THEN score + 10 ELSE score END;这种写法比写多条UPDATE语句更高效,但业务逻辑复杂的时候可读性会下降,需要配合注释使用。
更新的时候还有一个关键点:如果你更新多行,但中途某一行失败,前面的修改会怎样?这取决于事务。默认情况下,如果SQL语句本身出错,这一条语句执行的修改会全部回滚,不会只成功一半;如果你把它们放在一个显式事务里,就可以控制多条语句一起成功或一起失败。实际上,在命令行里逐条执行不带提交的UPDATE,MySQL默认是自动提交的,也就是说每执行一条,立刻生效,没有后悔药。所以更新数据之前,建议你先跑一遍相同的SELECT,确认要改的就是这些行:
-- 先确认 SELECT id, name, age FROM student WHERE class_name = '高一(1)班'; -- 再更新 UPDATE student SET age = age + 1 WHERE class_name = '高一(1)班';这个习惯看上去平平无奇,但能救你无数次。
5. DELETE:把数据抹掉
5.1 DELETE语法与安全性
DELETE用来删除表中的数据:
DELETE FROM student WHERE id = 1;跟UPDATE一样,DELETE最关键的也是WHERE。不写WHERE就是清空整张表:
-- 危险!删除所有学生 DELETE FROM student;关于DELETE,我见过的最典型事故是:想删一个学生,条件写错了,把整个班级的都删了;或者干脆忘了写WHERE,全表没了。预防办法就是前面说的“先SELECT后DELETE”,先查出来看看要删的是不是这些。
删除操作还有外键约束的问题。如果其他表里面有关联到student表的数据,DELETE的时候可能会报Cannot delete or update a parent row: a foreign key constraint fails。这是InnoDB在保护数据完整性:不能把别的表还在引用的父记录删掉。解决办法是先把子表里的关联数据处理掉,或者在外键上配置ON DELETE CASCADE,但这属于进阶内容,新手阶段先把概念搞清楚。
5.2 DELETE、TRUNCATE和DROP怎么选
很多新人会把“删除数据”和“删除表”混在一起。实际上有三个操作长得像但作用完全不同:
| 操作 | 作用 | 能否带WHERE | 事务支持 | 自增ID | 速度 |
|---|---|---|---|---|---|
| DELETE | 删除表数据 | 能 | 支持,可回滚 | 不会重置,继续递增 | 慢(逐行删除) |
| TRUNCATE | 清空表 | 不能 | 大多数情况下隐式提交,不可回滚 | 重置为初始值 | 快(直接重建表) |
| DROP | 删除整张表 | 不能 | 不可回滚(无备份的前提下) | — | 最快 |
一句话总结:想删部分数据,用DELETE;想清空表且希望自增ID重新从1开始,用TRUNCATE;想彻底删掉表结构和数据,用DROP。后续还能不能用这张表,取决于你是哪一种。
TRUNCATE的“不可回滚”需要特别强调一下。虽然MySQL 8.0的文档显示TRUNCATE在某些情况下可以回滚,但生产环境千万别赌这个行为。我身边真实案例:有同事用TRUNCATE清了一张日志表,后来发现数据还要用,因为没有备份,悔得肠子都青了。所以TRUNCATE和DROP这种高风险操作,执行前最好先备份。
6. 实战中常见的坑与排查
6.1 高频报错速查表
我在带新人和日常工作中,收集了这些MySQL新手高频报错,整理成表格,遇到问题可以对照着排查:
| 报错信息片段 | 含义 | 解决思路 |
|---|---|---|
| Unknown column 'xxx' in 'field list' | 字段名不存在 | 检查拼写,SHOW COLUMNS FROM 表名 |
| Table 'xxx' doesn't exist | 表不存在 | 检查是否选对数据库,USE xxx |
| Duplicate entry '1' for key 'PRIMARY' | 主键重复 | 主键自动生成,不要手动插入相同的值 |
| Incorrect string value | 字符集问题 | 表和字段都用utf8mb4 |
| Data too long for column | 值超长 | 检查字段长度定义是否合理 |
| Column count doesn't match value count | 插入的字段数和值数不一致 | 检查INSERT语句字段列表 |
| Cannot delete or update a parent row | 外键约束阻止 | 先处理子表数据 |
| You are using safe update mode | 缺少WHERE的安全保护 | 判断是否真的需要全表更新,或补充WHERE |
6.2 一定要养成的几个实操习惯
第一,生产环境操作前先备份。真正常犯的错不是SQL写错,而是没给自己留退路。哪怕只是更新一张小表,有备份就敢放手做,没备份就只能靠祈祈祷。
第二,养成看执行计划的习惯。可以简单一点:遇到查询变慢了,在SQL前面加个EXPLAIN。它会告诉你MySQL是怎么执行这条语句的,有没有用到索引,扫描了多少行。这是从“会写SQL”到“会写好的SQL”的分水岭。
EXPLAIN SELECT name, score FROM student WHERE age = 18;不用怕看不懂输出,先看type和rows两列就够起步了。type是ALL说明全表扫描,值得警惕;rows是预估扫描行数,越小越好。
第三,事务处理要心里有数。InnoDB支持事务,多步数据变更建议手动控制:
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; COMMIT;先跑一遍,确认没问题再COMMIT,有问题就ROLLBACK。这比一条条自动提交要安全得多。
第四,字段注释和SQL注释别偷懒。建表的COMMENT、SQL脚本里的注释,看起来是小事,但接手的人或者几个月后的你自己,会非常感谢这些注释。
我在实际使用中发现,很多数据库问题其实不是SQL语法不会写,而是对“这条语句到底会对哪些数据生效”缺乏感知。增删查改四个操作,最危险的不是写不出来,而是写出来之后一执行,影响的范围和自己想的不一样。所以每次写UPDATE和DELETE,我都会下意识地把前面的SELECT再执行一遍,确认范围之后再动手。这套习惯帮我避过了很多次潜在事故。
MySQL基础操作往后还能延伸很多方向,比如索引优化、事务隔离级别、存储过程、主从复制,但所有进阶内容都建立在今天这套CRUD的基础上。把增删查改练熟、把常见坑记住,后面学什么都会顺畅很多。建议你把这篇文章里的SQL语句照着敲一遍,敲完你就发现,数据库这事儿没有想象中那么神秘。