☰
MySQL增删查改全解析:从建库建表到CRUD实战与避坑指南
2026/10/13 7:47:27 网站建设 项目流程

在带新人或者做内部数据库培训时,我讲过最多的一句话就是:MySQL你会增删查改,就能搞定九成日常开发工作。这不是给基础操作贴金,而是事实——不管后面要学索引优化、事务隔离、主从复制还是分库分表,业务请求落到数据库这一层,最后都收敛成四个动作:插入、查询、修改、删除,也就是常说的增删查改(CRUD)。

这篇文章打算把MySQL里最基础的增删查改完整走一遍,从建库建表开始,把四种操作的语法、使用场景、背后的逻辑和实际开发里容易踩的坑一次性讲透。适合刚开始接触MySQL的同学,也适合写过一些SQL但对细节始终模棱两可、总被各种异常报错卡住的朋友。全文没有什么高深理论,都是我平时写接口、调数据、带项目时实际在用的操作方式和习惯。

1. 增删查改的底层逻辑与准备工作

1.1 为什么这四个操作构成了业务系统的地基

其实不需要举太复杂的例子,我们就想一个最常见的用户注册登录场景。新用户在前端页面填入信息点下注册按钮,后端接口收到请求后,要做的事情就是在用户表里插入一条新记录,这是增;用户登录时,系统要从用户表里查出这条记录并比对密码,这是查;用户哪天想改头像、换绑手机号,系统要更新对应用户的记录,这是改;最后用户要注销账号,或者管理员要清理违规账号,系统要从表里删除记录,这是删。

你看,一个完整的业务闭环就是由这四个动作组合出来的。任何一个业务系统,不管它的业务逻辑多复杂、表单字段有多少、页面跳转有多花哨,落到数据层就是这四个操作的不同排列组合。理解了这一点,你会发现SQL学习其实是有抓手的——先把增删查改的语法吃透,后面再学索引、事务、优化,都有了一个稳定的落脚点。换句话说,这四个操作的能力直接决定了你处理日常数据问题的下限。

1.2 库和表:操作要落在具体的对象上

增删查改是动词,它需要一个承载对象,也就是数据库和表。所以第一步,我们要先创建一个库,然后在库里建表。这里我推荐用MySQL 5.7以上版本或者8.0版本,8.0对SQL标准的支持更完整,默认字符集也更好用。我先给出一套完整的建表语句,后面所有的增删查改都围绕这张表展开:

CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE demo_db; CREATE TABLE IF NOT EXISTS users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键,自增', username VARCHAR(50) NOT NULL COMMENT '用户名', email VARCHAR(100) NOT NULL COMMENT '邮箱', age TINYINT UNSIGNED DEFAULT NULL COMMENT '年龄', city VARCHAR(50) DEFAULT NULL COMMENT '城市', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

这里有几个细节值得说一下。库的字符集我选的是utf8mb4而不是utf8,因为utf8在MySQL里最多只能存3字节的字符,像emoji表情和生僻字就存不进去,而utf8mb4是完整的4字节UTF-8编码,能覆盖所有Unicode字符。搞开发的如果一开始图省事选了utf8,等线上出现插入emoji报错的时候再迁移字符集,那才是真想哭。表引擎选择InnoDB,这是MySQL默认的存储引擎,支持事务、支持外键、崩溃恢复能力强,除非有非常特殊的理由,否则日常业务不用考虑MyISAM。

主键id用INT UNSIGNED AUTO_INCREMENT,这是最常见的一组搭配。INT不用多说,整数类型;UNSIGNED让它只能存非负数,这样可表示的范围从负21亿到正21亿变成0到42亿,等于翻了一倍;AUTO_INCREMENT让每次插入新记录时id自动加1,不用我们手动指定。username字段加上了NOT NULL约束和UNIQUE唯一约束,这个在后面的插入操作里会体现出它的作用——防止重复用户名注册。

2. 增:INSERT插入数据的门道

2.1 基础插入语法与列名选择

表建好了,接下来就是往里塞数据。插入操作的核心语句是INSERT,最基本的写法是这样:

INSERT INTO users (username, email, age, city) VALUES ('user_a', 'usera@example.com', 25, 'City_A');

这条语句执行后,users表里就多了一条记录,id自动变成1,created_at自动填充当前时间。注意,我在INSERT语句里显式列出了四列:username、email、age、city,没有列id和created_at,因为这两个字段都设置了默认值或者自增,MySQL会自动帮我们生成。这里有一个新手经常问的问题:列名到底要不要写?我的回答是,除非你确定所有列的值都要给全并且顺序和表结构完全一致,否则一定要写列名。写列名的好处是,你的INSERT语句和表结构的耦合度更低,将来表里增删字段,只要列名对得上,语句就不容易出问题。我在实际项目里见过不少直接不写列名的插入语句,一旦表结构调整过,这些语句大概率就报错了,排查起来的成本反而更高。

2.2 一次插多条数据与自增主键行为

实际开发里,我们经常需要往同一张表里批量插入数据。比如初始化测试数据、导入用户名单,一条一条插既不高效又容易出错。MySQL支持一次性插入多行:

INSERT INTO users (username, email, age, city) VALUES ('user_b', 'userb@example.com', 30, 'City_B'), ('user_c', 'userc@example.com', 28, 'City_C'), ('user_d', 'userd@example.com', 22, 'City_D');

这里有一个性能上的细节:多条VALUES记录用逗号分隔放在同一个INSERT语句里,相比逐条执行INSERT,网络往返次数从N次降到1次,在数据量大的时候效率提升非常明显。我自己在做数据迁移脚本时,通常会用一条INSERT拼几百甚至上千条VALUES,当然要注意SQL语句的长度,有个参数max_allowed_packet控制单条语句允许的最大包大小,默认一般是4M,大批量插入时如果报错提示packet too large,就需要调大这个值。另外可以留意一下,批量插入多条记录时,自增主键id会连续分配,中间不会因为某条插入失败而跳号,这一点在对接其他系统核对数据时会用到。

2.3 插入时的冲突处理

插入操作有一个很常见的业务场景:数据已经存在,但业务上希望“存在就更新、不存在就插入”。这个需求MySQL提供了INSERT ... ON DUPLICATE KEY UPDATE语法。以我们这张users表为例,username有唯一约束,所以如果我们想实现“用户名已存在就更新邮箱和城市,不存在就插入新记录”,可以这样写:

INSERT INTO users (username, email, age, city) VALUES ('user_a', 'usera_new@example.com', 26, 'City_E') ON DUPLICATE KEY UPDATE email = VALUES(email), age = VALUES(age), city = VALUES(city);

这条语句的效果是:如果插入的username在表里已经存在,唯一索引冲突,那么MySQL不会报错,而是执行后面的UPDATE子句,把email、age、city更新为新值;如果不存在,就正常插入。这种写法在做同步任务、数据导入场景里非常实用,省去了先查一次再决定插还是改的额外步骤。有一点要提醒,VALUES()函数在MySQL 8.0.20版本被标记为废弃,官方推荐使用新语法别名方式,比如:

INSERT INTO users (username, email, age, city) VALUES ('user_a', 'usera_new@example.com', 26, 'City_E') AS new ON DUPLICATE KEY UPDATE email = new.email, age = new.age, city = new.city;

如果你用的是8.0以上版本,建议直接写成这种新形式,避免未来升级时收到废弃警告。

2.4 插入失败的高频原因与预处理思路

插入数据最常见的报错无非这几种。第一种是字段长度超出定义范围,比如username字段定义的是VARCHAR(50),你非要塞一个100个字符的字符串,MySQL会报Data too long for column。第二种是NOT NULL字段没有给值,比如username字段有NOT NULL约束,你在INSERT语句里漏掉了它,就会报Field 'username' doesn't have a default value。第三种是唯一约束冲突,插入了重复的username,报Duplicate entry。

前两种比较好定位,看报错信息基本就能发现。第三种其实也算一种业务上的正常情况——说明你的防重复设计在起作用,这时候就要用我们上面讲的ON DUPLICATE KEY UPDATE或者INSERT IGNORE去处理。INSERT IGNORE和ON DUPLICATE KEY UPDATE的区别在于,前者遇到冲突直接忽略这条记录不报错,后者会执行更新操作。具体用哪个,取决于业务上遇到重复时是想保留旧数据还是覆盖新数据。我自己在做导入功能时,通常会先把原始数据做一次清洗和校验,把明显超长、缺必填项的数据提前拦截掉,再执行大批量INSERT,这样既能减少数据库端的报错,也让日志里的异常信息更有业务价值。

3. 查:SELECT查询的完整进阶

3.1 基础查询与WHERE条件过滤

查询是增删查改里使用频率最高、也是最灵活的操作。最基础的查询是SELECT * FROM users,意思是把users表里所有列所有行都拿出来。但在实际开发中,我不太建议在业务代码里用SELECT *。原因有两个:一是如果表字段很多,SELECT *会把不需要的字段也查出来,网络传输的数据量变大,浪费带宽;二是如果后续表结构加了新字段,应用代码里的对象映射可能无法处理,导致接口报错。正确的做法是用什么查什么:

SELECT id, username, email FROM users;

接下来是WHERE条件过滤。WHERE是SELECT的灵魂,它决定了你从表里取哪些行。比如我们要查所有年龄大于25的用户:

SELECT id, username, age, city FROM users WHERE age > 25;

又比如查城市在City_A或者City_B的用户:

SELECT id, username, city FROM users WHERE city IN ('City_A', 'City_B');

再比如模糊查询,搜用户名里包含“user”的用户:

SELECT id, username FROM users WHERE username LIKE 'user%';

这里的LIKE配合通配符是一种非常常用的模糊搜索方式。%代表任意多个字符,_代表任意单个字符。'user%'匹配以user开头的所有字符串,'%user'匹配以user结尾的,'%user%'匹配中间任意位置含有user的。要注意的是,LIKE模糊查询在数据量大时无法有效利用索引,尤其是前缀用了%的情况,性能会很差,在大表上做搜索类功能时尽量考虑全文索引或外部搜索组件。同时,IN后面也可以换成子查询或者参数列表,但要注意IN列表里的元素数量不要太多,过大时同样有性能隐患。

3.2 排序、去重与高效分页

查询结果默认不保证顺序,所以当我们希望数据按某个规则排列时,必须显式使用ORDER BY。比如按年龄从大到小排列:

SELECT id, username, age FROM users ORDER BY age DESC;

DESC表示降序,ASC表示升序,升序是默认值不写也行。多字段排序也支持,ORDER BY age DESC, id ASC的意思是先按年龄降序,年龄相同的再按id升序。这种多级排序在实际开发中很常见,比如排行榜既要看积分又要看达到积分的时间顺序。去重用DISTINCT,比如我们想看看users表里用户都分布在哪些城市:

SELECT DISTINCT city FROM users;

分页是查询操作里非常重要的一个场景。前端列表页、移动端下拉加载、后台管理系统的数据表格,几乎都要分页。MySQL的分页用LIMIT和OFFSET实现:

SELECT id, username FROM users ORDER BY id LIMIT 10 OFFSET 20;

这条语句的意思是从第21条记录开始取10条,也就是第二页的数据。LIMIT 10 OFFSET 20等价于LIMIT 20, 10,注意LIMIT后面第一个数字是偏移量,第二个数字是返回的记录数,这个顺序容易搞反。还有一个大表分页的性能问题是深分页,比如LIMIT 1000000, 10,MySQL需要先扫描前100万条记录再丢弃它们,代价非常高。常见的优化方式是记录上一页最后一条记录的id,用WHERE id > 上一次的id LIMIT 10来翻页,这样能走主键索引,效率高得多。

3.3 聚合函数与分组统计

在分析型查询里,我们经常需要对数据进行统计。MySQL提供了一组聚合函数,包括COUNT、SUM、AVG、MAX、MIN。比如统计用户总数:

SELECT COUNT(*) FROM users;

统计每个城市的用户数:

SELECT city, COUNT(*) AS user_count FROM users GROUP BY city;

这里COUNT(*)统计的是行数,AS用来给统计结果起别名,别名在结果集里显示为user_count。分组统计配合HAVING可以实现对分组结果的过滤,比如只显示用户数大于等于2的城市:

SELECT city, COUNT(*) AS user_count FROM users GROUP BY city HAVING user_count >= 2;

这里有一个关键的区分:WHERE是对分组前的原始数据行进行过滤,而HAVING是对GROUP BY分组后的结果进行过滤。这个区别面试经常问,实际写的时候也容易搞混。我个人的记忆方法是:WHERE在分组之前起作用,它能过滤单行,但不能引用聚合函数;HAVING在分组之后起作用,它专门配合聚合函数使用。另外,聚合函数里COUNT(*)和COUNT(某字段)也有细微区别,后者会忽略NULL值,统计时要注意字段是否可空。

3.4 多表联查:JOIN的适用场景

单表查询毕了,实际业务里更多的是多张表之间的关联查询。比如订单表orders关联用户表users,要查每个订单对应的用户名。JOIN就是为此而生的:

SELECT o.id AS order_id, u.username, o.amount FROM orders o INNER JOIN users u ON o.user_id = u.id;

这条语句把orders表和users表按user_id与id相等的关系连接在一起,只返回两个表都能匹配上的记录,这是INNER JOIN。如果想让左边的表全部返回,匹配不上的右边字段用NULL填充,那就用LEFT JOIN。还有RIGHT JOIN和FULL JOIN,不过FULL JOIN在MySQL里需要通过UNION来实现,实际用得很少,理解INNER和LEFT基本就够用了。

这里我特别想提醒一点:JOIN查询固然方便,但它同时也是SQL性能问题的重灾区。多表连接的本质是笛卡尔积过滤,如果连接条件写错或者漏写,产生了巨大的中间结果集,再配合上不合适的索引,一条查询就能把数据库拖垮。所以在写JOIN的时候,一定要确认ON关联字段两边的表都有索引,尤其是字段类型要一致,varchar和int做关联会让索引失效,这个细节我见太多人栽过了。

4. 改:UPDATE更新操作的坑与技巧

4.1 UPDATE的基本用法与不带WHERE的后果

更新操作的语法是UPDATE加SET加WHERE。假设用户user_a改了自己的邮箱,我们要把users表里user_a那条记录的email改掉:

UPDATE users SET email = 'usera_new@example.com' WHERE username = 'user_a';

和INSERT不同,UPDATE最危险的地方在于,如果你忘记写WHERE条件,或者WHERE条件写得太宽,那么UPDATE会更新表中符合条件的全部记录。不带WHERE就是全表更新:

UPDATE users SET email = 'usera_new@example.com';

这一条会把所有用户的邮箱都改成user_a的新邮箱。在生产环境执行这么一句,后果是灾难性的。我早年在测试库就踩过类似的坑,从那以后养成了两个习惯。第一个习惯是执行UPDATE之前,先用SELECT把WHERE条件查一遍,确认影响的行数和预期一致:

SELECT COUNT(*) FROM users WHERE username = 'user_a';

然后再执行UPDATE。第二个习惯是在MySQL客户端里开启安全更新模式,也就是执行SET SQL_SAFE_UPDATES = 1;,这个模式下MySQL会阻止不带WHERE条件或WHERE条件不包含索引列的UPDATE和DELETE语句执行。虽然日常开发环境这个模式偶尔会给你添麻烦,但它真的能在关键时刻拦住你犯低级错误。

4.2 多列更新与CASE WHEN批量更新

更新操作支持同时修改多个字段,这应该算是基本操作了:

UPDATE users SET age = 26, city = 'City_E' WHERE username = 'user_a';

多个字段之间用逗号分隔,这一点新手容易写成AND,注意SET后面多个赋值用逗号。批量更新也是一个高频需求。比如我们想根据用户名把不同用户的年龄改成不同的值,一条UPDATE语句可以配合CASE WHEN实现。假设三个用户分别要改成不同年龄,一次UPDATE做完:

UPDATE users SET age = CASE username WHEN 'user_a' THEN 26 WHEN 'user_b' THEN 31 WHEN 'user_c' THEN 29 END WHERE username IN ('user_a', 'user_b', 'user_c');

这种写法只用一条SQL就完成了多个用户的差异化更新,比逐条UPDATE发三次请求要高效得多。我一般在做后台批量编辑、定时任务修正数据时,都会优先考虑这种写法。要注意CASE WHEN的匹配是顺序匹配的,把条件更精确的放在前面,避免被前面的分支提前匹配掉。

4.3 UPDATE与事务控制

UPDATE还有一个绕不开的话题就是事务。InnoDB表支持事务,意味着我们可以把多个操作打包成一个原子单元,要么全部成功,要么全部回滚。在命令行或者客户端工具里,默认是自动提交模式,每一条UPDATE执行完立即生效。但在实际项目里,比如用户下单这个动作,可能需要更新订单表、扣减库存表、记录流水表,这个时候如果三条UPDATE只有前两条成功,第三步断电了,那数据就不一致了。所以这类操作必须在事务中执行:

START TRANSACTION; UPDATE orders SET status = 'paid' WHERE order_id = 1001; UPDATE products SET stock = stock - 1 WHERE product_id = 2002; COMMIT;

如果第二步执行过程中发现库存不足或者其他异常,可以执行ROLLBACK把整个事务回滚,数据库会恢复到事务开始之前的状态。有一点要特别注意:事务里的UPDATE会持有行锁,直到COMMIT或者ROLLBACK才释放。如果你开了事务忘记提交,还一直占着连接,别人想更新同一行记录就会被阻塞住,严重的会造成线上大量请求堆积超时。我自己排查过很多次“数据库突然变慢”的问题,最后发现是有开发者手动开了事务忘了提交,这种低级错误真的非常常见。排查时可以执行SHOW PROCESSLIST;查看是否有长时间处于事务状态的连接,找到对应的连接处理后,问题通常很快就能缓解。

5. 删:DELETE与TRUNCATE的抉择

5.1 DELETE的基本用法与误删防护

删除操作最基础的语法是DELETE FROM加WHERE。比如删除user_a这条记录:

DELETE FROM users WHERE username = 'user_a';

我们前面在UPDATE那里强调过的“WHERE条件必须谨慎”在DELETE这里更加重要,因为DELETE一旦执行,数据就真的没了。不带WHERE条件的DELETE会清空整张表:

DELETE FROM users;

执行完这句后,users表里一条数据都不剩。我非常建议删除操作同样在安全更新模式下执行,并且先SELECT确认要删除的行数。有一点很多人不知道的是,DELETE在InnoDB引擎下是可以配合事务回滚的。如果你先执行START TRANSACTION,再执行DELETE,发现删错了数据,在COMMIT之前执行ROLLBACK,数据还能恢复回来。这也是一条保命技巧。我自己在做一些批量清理数据的操作时,习惯先把操作放在事务里不提交,确认无误后再COMMIT。

5.2 DELETE与TRUNCATE的区别

删除表数据还有一个TRUNCATE语句,写法是:

TRUNCATE TABLE users;

TRUNCATE和DELETE的区别需要认真理解。第一,TRUNCATE是DDL语句,执行后立即生效,无法在事务里回滚;而DELETE是DML语句,可配合事务回滚。第二,TRUNCATE会重置自增计数器,也就是说删除后下次插入的id从1开始;DELETE不会重置,即使表被删空了,下次插入的id也会接着之前的继续自增。第三,TRUNCATE执行速度通常比DELETE快很多,因为它不逐行删除,而是直接丢弃整个表的数据页,但TRUNCATE不能像DELETE那样精确删除部分行。

所以实际的选型逻辑是:清空一张表的全部数据用TRUNCATE,速度快、干净利落;删除部分数据用DELETE,可以按条件精确控制;如果要删除的数据行数非常多,比如超过百万级别,DELETE一条一条删除加事务日志的成本很高,这时候要么分批删除,要么TRUNCATE全表重建,要么直接DROP表再重建。至于在业务代码里,绝大部分时候用的都是DELETE加上精确条件,TRUNCATE更适合做临时表清理和测试环境数据重置。

5.3 删除操作要避开的外键和关联问题

如果表之间有外键关系,删除操作会变得稍微复杂。比如orders表里有一条订单引用了users表的id,你直接去DELETE users表里对应的用户记录,MySQL会报外键约束错误。处理方式一般有两种:第一种是先删除关联表中的子记录,再删除父表记录;第二种是在建表时就设置外键级联删除,比如在orders表的外键上写ON DELETE CASCADE,这样删除父表记录时子表关联记录自动删除。

我建议在生产环境谨慎使用级联删除。虽然它用起来方便,但风险在于你可能没有完全意识到有多少张子表关联着这条父记录,一个DELETE下去子表哗啦啦删掉一片,想恢复都难。我见过的稳妥做法是,业务代码中显式控制删除顺序,先查关联,再分别删除,必要的时候用逻辑删除代替物理删除——也就是给表加一个is_deleted字段,删除操作实际变成UPDATE把is_deleted置为1。这样数据永远在库里,只是查询时过滤掉,出问题还能溯源,很多对数据安全要求高的系统都倾向于用逻辑删除。当然,逻辑删除会让查询条件变得复杂一些,每次都要记得带上is_deleted = 0这个条件,这也是它的代价。

6. 结合模拟项目:一个完整增删查改流程

6.1 场景设定与初始数据

为了把前面的内容串起来,我设计一个很简单的模拟项目X:一个用户积分商城系统的用户模块。初始数据如下。用户表:user_a年龄25,城市City_A;user_b年龄30,城市City_B;user_c年龄28,城市City_C;user_d年龄22,城市City_D;user_e年龄35,城市City_E。积分表里,user_a有1200分,user_b有800分,user_c有1500分,user_d有600分,user_e有3000分。我们需要完成的需求是:批量插入初始数据、按条件查询用户、修改某个用户的城市、删除某个用户、统计各城市用户数。

6.2 实操流程与执行记录

第一步,建表:

CREATE TABLE IF NOT EXISTS users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, city VARCHAR(50), age TINYINT UNSIGNED, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS points ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, points INT UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第二步,批量插入用户数据:

INSERT INTO users (username, city, age) VALUES ('user_a', 'City_A', 25), ('user_b', 'City_B', 30), ('user_c', 'City_C', 28), ('user_d', 'City_D', 22), ('user_e', 'City_E', 35); INSERT INTO points (username, points) VALUES ('user_a', 1200), ('user_b', 800), ('user_c', 1500), ('user_d', 600), ('user_e', 3000);

第三步,查询所有年龄大于等于25的用户,按年龄降序排:

SELECT username, city, age FROM users WHERE age >= 25 ORDER BY age DESC;

第四步,user_a搬家去了新的城市,更新他的城市:

UPDATE users SET city = 'City_F' WHERE username = 'user_a';

第五步,user_d注销账号,删除他以及他的积分记录:

DELETE FROM points WHERE username = 'user_d'; DELETE FROM users WHERE username = 'user_d';

第六步,统计每个城市用户数量:

SELECT city, COUNT(*) AS user_count FROM users GROUP BY city;

这一套流程走完,你就把增删查改全部实际操作了一遍。这看起来简单,但每一条语句背后都有讲究。如果你能顺手把每一步的执行计划也看一下,比如在UPDATE、DELETE之前用SELECT验证数据,你会比大多数只会抄SQL的开发者稳健得多。后面可以在这个基础上试着给表加索引、观察执行计划EXPLAIN,你会发现海阔天空。整个模拟项目的代码也很适合自己动手敲一遍,不要复制粘贴,敲一遍和看一遍的体感差别非常大。

7. 常见问题与排查技巧实录

7.1 高频报错速查表

报错信息出现原因解决方案
Data too long for column 'username'插入的数据超过了字段定义长度检查数据长度,或扩大字段长度VARCHAR
Field 'username' doesn't have a default valueNOT NULL字段没给值插入时显式给字段赋值
Duplicate entry 'user_a' for key 'users.username'唯一约束冲突用ON DUPLICATE KEY UPDATE或先查询判断
Unknown column 'xx' in 'where clause'WHERE条件里写了不存在的字段检查字段名拼写
You are using safe update mode安全更新模式下执行了没有索引条件的UPDATE/DELETE加索引列条件,或临时SET SQL_SAFE_UPDATES=0
Lock wait timeout exceeded事务未提交导致锁等待超时检查未完成的事务,确认后COMMIT或ROLLBACK

这个表里的几个错误,是我在带新人时最常看到的。尤其是锁等待超时和重复键冲突,几乎每个团队都会遇到。排查锁问题有一个很实用的命令:SHOW PROCESSLIST;,它能列出当前所有连接和正在执行的SQL,你能直接看到哪些连接在等待锁、哪些事务一直没提交。还有一个思路是查看INNODB_TRX表,执行SELECT * FROM information_schema.INNODB_TRX;,可以列出当前所有未提交的事务,结合trx_started时间,很快就能定位是谁长时间持有事务不提交。

7.2 我的一些实用经验与小建议

最后聊几个我实际项目里总结的习惯。第一个习惯是:在写UPDATE和DELETE之前,永远先写SELECT。哪怕你对WHERE条件很有信心,也先查一下,看看影响的行数是不是你预想的那行。这不是浪费时间,这是在给自己上保险。尤其在生产环境,一个小小的失误带来的连锁反应可能让你一整天都在收拾烂摊子。

第二个习惯是:充分利用DESC命令查看表结构,随时了解字段类型、是否允许NULL、有没有默认值。很多插入报错,其实都是因为对表结构不熟。执行DESC users;,字段信息一目了然,比靠记忆猜字段靠谱得多。

第三个习惯是:把常用的增删查改SQL放到一个本地的SQL脚本文件里。当你需要快速操作或者排查问题时,打开脚本直接改条件值执行,效率比自己手敲高很多,也不容易打错字段名。第四个习惯是:学会使用EXPLAIN查看查询执行计划。哪怕是新手,也可以对一条SELECT加上EXPLAIN前缀,看看它的type、rows、key这些字段。你会发现,有时一条简单的查询,走了全表扫描和走索引的性能差距可能是百倍。理解了执行计划,你写SQL的时候就会下意识考虑索引,这比背优化原则有效得多。

我自己在写完增删查改之后,最喜欢做的一件事是故意制造错误——比如故意漏WHERE条件,故意插入重复数据,故意让字段超长,然后看MySQL报什么错、错误信息是什么样的。出错的情况遇得多了,线上遇到问题才不至于慌。数据库的操作说到底就是一个熟练度的问题,多练、多踩、多想,基础扎实了,后面学索引、学事务、学优化都会轻松很多。希望这篇基础操作的内容能帮你把增删查改这四板斧真正练扎实,练到成为肌肉记忆,到时候看什么业务场景,其实都万变不离其宗。

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

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

立即咨询