☰
MySQL DML语句详解:INSERT、UPDATE、DELETE、SELECT与事务实战
2026/10/19 5:10:52 网站建设 项目流程

1. 先搞清楚DML的管辖范围:和DDL、DCL不是一回事

很多零基础的朋友刚开始学MySQL,最大的困惑不是语句本身难写,而是搞不清这一堆命令到底谁管谁。建表的时候用CREATE TABLE,授权的时候用GRANT,往表里塞数据又不知道叫什么名字,打开教程一看,一会儿DDL一会儿DML一会儿DCL,直接看晕。

我这里用一句话给你定位:DML全称是Data Manipulation Language,数据操作语言,管的是表里面“已经存在的数据怎么增、怎么删、怎么改、怎么查”。它不负责表的定义,也不负责权限控制,那些分别属于DDL(Data Definition Language,数据定义语言)和DCL(Data Control Language,数据控制语言)的范畴。

1.1 一张表分清SQL的三大类

为了让你心里有个整体坐标系,先看这张对照表,后面章节所有的内容都不会跑出这个框架:

类别全称管的范围典型语句
DDLData Definition Language数据库、表、索引、视图的结构定义与修改CREATE、ALTER、DROP、TRUNCATE
DMLData Manipulation Language表内部数据的增删改查INSERT、UPDATE、DELETE、SELECT
DCLData Control Language用户权限、访问控制GRANT、REVOKE

有人可能会问:SELECT不是查询吗,为什么也算DML?这里有个知识点容易有争议。从严格的SQL标准来说,SELECT属于DQL(Data Query Language,数据查询语言),但在MySQL官方文档和绝大多数入门教学体系里,都把SELECT放进DML一起讲。原因很实际:你写业务代码的时候,90%以上的场景是把SELECT和INSERT、UPDATE、DELETE混在一起用的,比如先查一下库存再决定要不要更新,查完发现数据不对再删掉重来。把SELECT单独抽出去反而割裂了学习路径,所以这套教程沿用MySQL的习惯,四兄弟一起讲。

1.2 为什么这一章对你最重要

DDL说白了就是建房子,建一次基本不动;DCL是发钥匙,只在分配权限时用。但DML是你每天都在敲的语句——注册用户往表里插一条记录,改头像要更新一条记录,注销账号要删除一条记录,打开首页要查一堆记录。

可以这么说:DDL决定数据长什么样,DML决定数据怎么活。你要是把DML学扎实了,后面学索引优化、事务隔离、读写分离,都是在DML这条主线上做文章。反过来,DML不熟,你连排查问题的资格都没有——报错你都看不懂是语法错误还是数据问题。

我见过不少半路转行的朋友,上来先啃索引原理,什么B+树、聚簇索引背得滚瓜烂熟,结果让他写一条既改库存又记流水账的SQL,手忙脚乱。原因就是DML的基本功不牢。所以这一章别求快,跟着示例一条一条敲,敲完用SELECT验证结果,比光看不练强十倍。

2. INSERT插入数据:三种写法覆盖九成场景

INSERT是DML里的“加法”,负责往表里塞新数据。大部分刚入门的人只知道INSERT INTO 表名 VALUES (...)这一种写法,但实际开发中,这种写法恰恰是使用频率最低、最容易踩坑的。

先建一张演示表,后面所有示例都在这张表上操作,你可以直接在本地MySQL里跟着敲:

CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT DEFAULT 18, city VARCHAR(50), score DECIMAL(5,2) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这张表模拟了一个学生基础信息表,id是自增主键,age有默认值18,score是带两位小数的成绩字段。下面三种写法,你按场景选。

2.1 全字段插入:最直观但最脆弱的写法

INSERT INTO student VALUES (1, '张三', 20, '北京', 89.5);

这种写法不用指定字段名,但要求VALUES里必须包含表结构里所有字段的值,一个不多一个不少,顺序还得和建表时完全一致。

它的脆弱点在于:只要表结构一变,这条SQL就可能立刻失效。比如你后来往表中间加了一个字段,那么所有按旧结构写的INSERT都会报列数不匹配;再比如你依赖id自增,却非要在VALUES里写死1,下次插入就可能撞主键。

按我以前带新人的经验,这种写法最容易在测试环境“跑通了”,到生产环境一执行就报错。因为测试表和生成表的字段经常不一样。所以它只适合你临时手工往表里塞几条数据验证一下,写正式代码不要用它。

2.2 指定字段插入:正式开发的首选

INSERT INTO student (name, age, city, score) VALUES ('李四', 22, '上海', 92.0);

这里只写了name、age、city、score四个字段,id完全交给了自增机制。没提到的字段,MySQL会按建表时定义的规则处理:有DEFAULT就用默认值,允许NULL就填NULL,NOT NULL且没有默认值就会直接报错。

这种写法有几个明显好处:

  • 不依赖字段顺序:VALUES里的值只和你括号里写的字段顺序对应。
  • 结构变更更友好:新增一个可有可无的字段,只要INSERT里没提它,SQL照样能跑。
  • 语义清晰:别人一看就知道你在给哪些列赋值,代码可读性高很多。

所以我在项目里对团队的要求很明确:除了手动排查临时数据,一律使用指定字段插入。

2.3 批量插入:一条语句搞定多行数据

INSERT INTO student (name, age, city, score) VALUES ('王五', 21, '广州', 85.0), ('赵六', 23, '深圳', 78.5), ('孙七', 20, '杭州', 90.0);

一次性插入多行,用逗号分隔,这是批量导入数据最常用的手段。相比一条一条执行单行INSERT,这种写法把多次网络往返合并成一次,效率成倍提升。尤其在你用程序批量导入Excel、CSV数据时,能把几千条数据分批次拼成这种多行VALUES,几百条一批,性能非常可观。

还有两种变体也提一下。一种是INSERT INTO ... SET写法:

INSERT INTO student SET name='周八', age=19, city='成都', score=88.0;

这种写法的好处是字段和值一一对应,有点像UPDATE,适合插入字段少的时候看;坏处是不支持批量插入多行,字段多起来语句会很冗长。

另一种是插入的同时处理重复键冲突:

INSERT INTO student (id, name, age, city, score) VALUES (1, '张三', 20, '北京', 89.5) ON DUPLICATE KEY UPDATE score = 89.5;

这个语句专门用来处理“主键或唯一键冲突”的场景:如果id=1的记录不存在,就正常插入;如果已经存在,就执行UPDATE后面的逻辑。日常业务里“有则更新、无则插入”的需求非常多,比如统计流水、同步用户信息,用这个语句省掉你先SELECT判断再决定INSERT还是UPDATE的啰嗦流程。

2.4 插入数据时最容易撞的四个坑

操作过程中有几个坑,我建议你提前记下来:

第一,中文乱码。插入中文数据后查出来是乱码,九成是表的字符集不是utf8mb4,或者你的客户端连接字符集不对。建表时统一用utf8mb4,连接后先执行SET NAMES utf8mb4;,基本能杜绝这个问题。

第二,自增列不要手动指定。除非你有明确业务原因要在指定id,否则别在INSERT里写自增主键的值。让MySQL自己分配可以避免主键冲突,也能保证id连续性不受干扰。

第三,NOT NULL字段漏填。比如student表的name字段是NOT NULL,insert时漏了name,MySQL直接报Field 'name' doesn't have a default value。看到这个错别懵,就是某字段既不能为NULL又没有默认值,你还没给它赋值。

第四,字段值超长度。VARCHAR(50)你非要塞60个字符,MySQL在严格模式下会直接报错,在非严格模式下会截断。强烈建议开启严格模式,宁可直接报错也不能让数据悄悄被截断。

插入完了怎么确认?别全靠猜,一条SELECT就搞定:

SELECT * FROM student;

看到数据一条条列出来,说明INSERT成功,这就算完成了第一次完整的数据闭环。

3. UPDATE更新数据:最危险的一条SQL,没有之一

要说DML里哪条语句最危险,我毫不犹豫投UPDATE一票。DELETE至少你心里知道它是危险的,写的时候会多留个心眼,而UPDATE看着人畜无害,经常一条下去,把全表数据改得面目全非。

3.1 带不带WHERE,是两个世界

先看基础语法:

UPDATE student SET score = 90.0 WHERE id = 1;

这条语句把id等于1的学生的成绩改成了90.0。关键在于WHERE,它限定了“只更新哪些行”。

但是如果你把WHERE忘掉:

UPDATE student SET score = 90.0;

恭喜你,全表所有学生的成绩都变成了90.0。没有WHERE条件的UPDATE,就等于告诉MySQL“这张表里所有行都给我改成这个值”。

大多数初学者第一次犯这个错,都是在测试环境,数据本身不重要,顶多笑一下删掉重来。但我在工作中真见过因为没写WHERE,直接UPDATE了线上所有用户余额的——几个小时后用户发现余额不对,全公司排查,最终靠备份恢复数据,过程极其痛苦。所以每次写UPDATE,养成先写WHERE再写SET的习惯,哪怕你确定就是要更新全表,也要先写个WHERE 1=1之类的条件让自己看见。

3.2 多字段更新与计算更新

UPDATE一次可以同时更新多个字段,字段之间用逗号分隔:

UPDATE student SET age = age + 1, city = '南京' WHERE name = '张三';

这条语句把张三的年龄加1岁,城市改成南京。注意age = age + 1这种写法,它表示“在当前age的基础上加1”,这是非常常见的一类需求,比如积分加1、库存减1、登录次数累加,都可以直接用这种计算赋值。

这里有一个并发环境下的经典要点:如果直接用SET stock = stock - 1,在并发很高的情况下,数据库的行锁会保证这个操作的原子性,不会出现两个请求同时读到stock=10,然后都写回9的情况。反过来,如果写成“先SELECT拿到stock,程序里减1,再UPDATE写回”,并发高时容易丢失更新。能一步在SQL里做的计算,就不要拆成两步在程序里做,这是我从实战里得出的经验。

UPDATE也可以配合子查询,比如“把成绩低于平均分的学生城市改成‘待定’”:

UPDATE student SET city = '待定' WHERE score < (SELECT AVG(score) FROM student);

这个例子告诉你,UPDATE的WHERE条件不限于简单的等值判断,它可以像SELECT一样使用各种运算符、子查询、函数。逻辑能力有多强,UPDATE就能写多复杂。

3.3 配合LIMIT限制更新范围

MySQL的UPDATE还支持LIMIT子句,这个特性在别的数据库里少见,但在实际运维和数据处理时特别有用:

UPDATE student SET score = 80.0 WHERE city = '北京' LIMIT 3;

这条语句会把北京的学生里找出前3条(按物理存储顺序),只更新这3条的成绩。这种写法在按批次修正数据时很好用,比如你有一百万条脏数据要修复,每次只更新1000条,可以分批执行,避免一次性锁太多行导致线上业务卡顿。

注意,LIMIT通常要配合ORDER BY才可控,不然你根本不知道更新的是哪几条:

UPDATE student SET score = 80.0 WHERE city = '北京' ORDER BY id ASC LIMIT 3;

这样就明确规定了“按id从小到大排序后取前3条”,可预测、可复现。

3.4 更新前先查询:一条能保命的经验

我个人的实操习惯是,在做重要UPDATE之前,永远先用SELECT预览一下将要影响的范围:

SELECT * FROM student WHERE city = '北京' ORDER BY id ASC LIMIT 3;

确认这条WHERE条件圈出来的记录,确实是你想要的那些,再执行对应的UPDATE。这一步多花十秒钟,可能帮你省掉十个小时的恢复工作。

另外提醒一句,UPDATE执行的返回结果会显示“Query OK, 1 row affected”,这个1就是受影响的行数。如果你明明知道条件里有几十条匹配,结果却是0 row affected,那就要回头检查WHERE条件是不是写错了,或者数据本身和预期不一致。

4. DELETE删除数据与TRUNCATE的差别:别把路走窄了

DELETE负责删数据,但它比你想象中要“温柔”得多。很多新手以为DELETE就是彻底把数据抹掉了,其实远不是这么回事。

4.1 DELETE的基础用法:WHERE依然是命根子

DELETE FROM student WHERE id = 5;

这条语句删除id等于5的学生记录。和UPDATE一个道理,不带WHERE的DELETE会把整个表的数据全部清空:

DELETE FROM student;

清空之后,表结构还在,自增计数器也不会重置,你下次再插入数据,id会从上次的最大值继续往上走。这一点和TRUNCATE有本质差别,下面单独说。

从安全角度看,DELETE比UPDATE更凶险,因为删除之后要恢复只能靠备份或日志。所以我在教新人的时候反复强调:执行DELETE之前,你的流程里必须包含“备份”这一步。小表直接:

CREATE TABLE student_bak_20250101 AS SELECT * FROM student;

大表就做逻辑备份或者导出到文件。有了备份再删,心里才有底。

4.2 DELETE与TRUNCATE:一场经典的对比

很多人知道TRUNCATE也能清空表,但不知道两者有什么区别,这里给一张对比表:

对比项DELETETRUNCATE
是否可加WHERE可以,按条件删除不可以,只能全表清空
是否记录行明细逐行删除,记录每一行直接释放数据页,不逐行记录
是否重置自增id不重置重置
是否支持事务回滚在事务内可以回滚执行后无法按行回滚
执行速度慢,数据量大时很慢快,几乎瞬间完成
是否触发删除触发器会触发不会触发

举两个典型例子:

删除部分数据,比如只删除成绩低于60分的学生,必须用DELETE:

DELETE FROM student WHERE score < 60;

清空一张临时表、历史日志表并且希望自增id从1重新开始,用TRUNCATE更合适:

TRUNCATE TABLE student;

这条语句会快速把所有行删掉,同时把自增计数器归零。如果表很大,TRUNCATE比DELETE快好几个数量级,原因是TRUNCATE直接释放整张表的数据页,而DELETE是一条一条标记得删除。

4.3 DROP和DELETE别混:drop删的是表本身

在入门阶段,还有一个词经常和DELETE混在一起——DROP。这里明确一下:

DROP TABLE student;

DROP删的是整张表,包括表结构和数据,表直接不存在了。DELETE删的是表中的行,表本身还在。三者层级关系是:DROP > TRUNCATE > DELETE。顺序千万别弄反,也别在同学面前说出“我drop一条数据”这种外行话。

4.4 删除数据前为什么建议先SELECT确认

和UPDATE一样,我强烈建议在DELETE前先用SELECT预览:

SELECT * FROM student WHERE score < 60;

然后再执行:

DELETE FROM student WHERE score < 60;

这两条SQL的WHERE条件完全相同,只是前面的查询命令换成了删除命令。很多成熟团队甚至会在代码规范里规定:涉及生产数据的DELETE,必须经过审批,审批材料里必须包含SELECT预览结果。

删除之后同样用SELECT验证:

SELECT * FROM student WHERE score < 60;

结果为空,说明删除生效。强迫自己养成“删除三步曲”——备份、预览、执行后验证,以后你会感谢这个习惯。

5. SELECT查询数据:DML里的重头戏

虽然标题是“DML语句定义及常见用法示例”,但SELECT在实际工作中和增删改是绑定的,没有查询能力,前面学的INSERT、UPDATE、DELETE都像在盲人摸象。所以这一章花一半篇幅讲SELECT完全值得。

5.1 SELECT的基本骨架:别让*符号害了你

最简单粗暴的查询:

SELECT * FROM student;

这条语句把student表所有字段、所有行全查出来。星号代表“所有列”,在表结构简单、数据量小的时候用起来很爽,但有两个隐患:

  • 数据量大时,SELECT * 会白白传输你根本用不到的字段,浪费内存和带宽。
  • 表结构一旦变更,比如加了几个大字段,程序里用“按位置取值”的代码就会出错。

所以在实际项目里,更推荐按需查询:

SELECT id, name, score FROM student;

这样只拿你关心的三个字段,代码意图清晰,执行效率也更高。

5.2 WHERE过滤:从全表扫描到精确定位

查询的核心就是过滤。刚刚学的比较运算符、逻辑运算符,在这里全面派上用场:

SELECT id, name, score FROM student WHERE score >= 85 AND city = '上海';

这条语句筛选出上海且成绩不低于85分的学生。WHERE后面可以接等值判断(=)、范围判断(>、<、>=、<=、<>)、模糊匹配(LIKE)、集合判断(IN、NOT IN)、空值判断(IS NULL、IS NOT NULL)。

常见几个例子一并展示:

-- 查询成绩在80到90之间的学生 SELECT id, name, score FROM student WHERE score BETWEEN 80 AND 90; -- 查询城市是北京或上海的学生 SELECT id, name, city FROM student WHERE city IN ('北京', '上海'); -- 查询名字以“张”开头的学生 SELECT id, name FROM student WHERE name LIKE '张%'; -- 查询没有填写成绩的学生 SELECT id, name FROM student WHERE score IS NULL;

这里要特别提醒一个新手常犯的错误:空值判断必须用IS NULL,不能用= NULL。在SQL里,score = NULL永远不会返回结果,因为NULL表示“未知”,和任何值比较的结果都是NULL,而NULL在WHERE里被当作假。我见过不少人在这个问题上卡了一整天。

5.3 排序与分组:让数据说话

查询查出来的数据往往是无序的,要让它有逻辑,用ORDER BY:

SELECT id, name, score FROM student ORDER BY score DESC, id ASC;

先按成绩从高到低排,成绩相同的按id从低到高排。DESC是降序,ASC是升序,默认就是升序,所以ORDER BY score等价于ORDER BY score ASC。

分组是另一个高频操作,结合聚合函数使用:

SELECT city, COUNT(*) AS student_count, AVG(score) AS avg_score FROM student GROUP BY city;

这条语句按城市分组,统计每个城市的学生人数和平均分。GROUP BY后面跟的字段,必须出现在SELECT列表中,或者被聚合函数包裹,这是SQL的规则,违反了会报错。

如果要筛掉某些分组,用HAVING,它不是WHERE的替代品,而是作用在分组结果上的过滤器:

SELECT city, AVG(score) AS avg_score FROM student GROUP BY city HAVING AVG(score) >= 85;

这条只显示平均分不低于85分的城市分组。

5.4 LIMIT分页:查数据不能一次全倒出来

数据量一大,没人会一次性把几十万行全部查出来展示到页面上。LIMIT负责“取前N条”或者“分页跳过若干条”:

-- 取成绩最高的前5名 SELECT id, name, score FROM student ORDER BY score DESC LIMIT 5; -- 跳过第1页的5条,取第2页的5条 SELECT id, name, score FROM student ORDER BY id ASC LIMIT 5 OFFSET 5;

第一句很好理解,第二句里OFFSET 5表示“跳过前5条”,然后LIMIT 5表示“取5条”,合起来就是6到10条。很多编程框架生成分页SQL时会自动套LIMIT和OFFSET,你看到LIMIT 10, 20这种写法也别慌,它的意思是OFFSET 10、LIMIT 20,也就是跳过10条取20条。

分页查询有个性能坑提前告诉你:页码越深,OFFSET越大,MySQL要扫描并丢弃前面的所有行,性能急剧下降。后续学索引优化时,你会接触到“基于游标分页”的替代方案,现阶段先把LIMIT语义搞清楚就行。

6. DML操作与事务:数据一致性从哪来

增删改查都学完了,但还差最后一个环节:这些操作能不能被撤销?能不能保证多条语句“一起成功或者一起失败”?

6.1 为什么DML离不开事务

想象一个最典型的场景:转账。从A账户扣100元,往B账户加100元,这需要两条UPDATE语句:

UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; UPDATE account SET balance = balance + 100 WHERE user_id = 'B';

第一条执行成功、第二条执行失败会怎样?A的钱少了100,B的钱没增加,系统整体就乱了。事务(Transaction)就是来解决这个问题的:把多条DML操作打包成一个整体,要么全部成功,要么全部回滚,不存在“做了一半”的状态。

6.2 三个关键词:BEGIN、COMMIT、ROLLBACK

MySQL里事务控制的核心就三个命令:

BEGIN; -- 开启一个事务 UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; COMMIT; -- 确认提交,让改动永久生效

如果在执行中途发现错了,不想让任何改动生效:

BEGIN; UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; ROLLBACK; -- 回滚,撤销刚才的所有改动

ROLLBACK之后,这两条UPDATE的影响全部消失,相当于从来没执行过。

还有不同状态的COMMIT和ROLLBACK在事务里的位置很讲究。默认情况下MySQL是自动提交模式,也就是每条DML语句执行完就自动提交,不需要你手动COMMIT。但一旦你写了一个BEGIN开启事务,后续所有DML语句都不会自动提交,直到你执行COMMIT或ROLLBACK为止。

在程序开发中,我建议的项目级规范是:凡是涉及多张表联动修改的DML操作,必须显式开启事务,并在finally或defer逻辑里做好回滚兜底。你以后用Python的pymysql、Java的JDBC或MyBatis,都会看到事务的身影,事务是DML的安全网。

6.3 DDL自动提交:一个容易被忽略的坑

有一类语句会“偷偷”把事务结束掉,同时把当前事务里还没提交的改动直接提交,这类语句主要是DDL,比如CREATE TABLE、ALTER TABLE、DROP TABLE、TRUNCATE TABLE。

举一个极其常见的悲剧场面:

BEGIN; UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; -- 某同事不小心在这中间执行了: -- ALTER TABLE account ADD COLUMN remark VARCHAR(100); -- 你以为还能ROLLBACK,实际上事务已经被隐式提交了 ROLLBACK;

执行ALTER TABLE时,MySQL会先把当前事务自动提交,然后执行DDL。所以前两条UPDATE已经永久生效了,ROLLBACK根本救不回来。

这正是我为什么说,INSERT、UPDATE、DELETE的表结构,尽量在业务开始前一次性做好,不要在事务运行到一半时做ALTER。否则数据一致性和你的预期会差得很远。

6.4 用演示走一遍事务的完整流程

回到student表,模拟一个最简单的操作:把张三的成绩+5,再给李四的成绩+3,然后验证两个改动是否同时生效。

SELECT * FROM student WHERE name IN ('张三', '李四'); BEGIN; UPDATE student SET score = score + 5 WHERE name = '张三'; UPDATE student SET score = score + 3 WHERE name = '李四'; COMMIT; SELECT * FROM student WHERE name IN ('张三', '李四');

如果你在COMMIT之前把第二条UPDATE故意写错,比如字段名写成scroe,MySQL会报错,此时事务还没有提交,你可以立刻ROLLBACK,张三第一条改动也随之撤销。这就是“要么全成功、要么全回滚”的直观体验。

事务这部分内容远不止这些,隔离级别、锁机制、MVCC都是后面章节的重头戏。但现阶段你只需要记住:多条DML组合操作时,用事务把它们捆在一起,必要的时候用ROLLBACK兜底。把这一条贯彻到位,你就已经比很多凭感觉写增删改的开发者专业了。


从INSERT到UPDATE,从DELETE到SELECT,再到用事务把DML操作拧成一股绳,这一章把DML的骨架搭完了。根据我个人经验,学到这里最忌讳的就是“眼会手不会”。建议你跟着每一条示例,在自己的MySQL里把student表建出来,把三种INSERT写法都跑一遍,把不带WHERE的UPDATE和DELETE各自试一次(在测试表上),亲身体验一下“全表被改”的震撼,下次写代码才会本能地先摸WHERE。

最后再分享一个小技巧:以后你在命令行或客户端里执行重要DML语句之前,如果心里有一丝丝不确定,就先在原SQL前面加个SELECT,把WHERE条件跑一遍。这个习惯陪我走过了很多线上环境,价值比你想象的还要大。下一章,我们自然就会聊到数据过滤与排序的更多玩法,到时候你已经有了这批数据基础,进阶起来会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询