☰
MySQL增删查改全攻略:库表与数据行的DDL和DML详解
2026/10/13 7:47:27 网站建设 项目流程

MySQL是很多开发者的第一套数据库,日常开发里接触最多的就是增删查改这四个动作。前阵子帮一位刚转行的朋友看代码,他写了半天建表语句,又叫它“插入数据”,我就意识到不少人对“增删查改”的理解是模糊的:到底是对库操作、对表操作,还是对数据行操作?MySQL里这三层对象的语法长得像,含义完全不同。这篇文章我拿一张会员积分表做例子,从连接数据库开始,把库、表、数据三个层面的增删查改完整过一遍,重点讲INSERT、SELECT、UPDATE、DELETE这四条高频语句的写法、参数细节和我实际踩过的坑。适合刚入门数据库的新手,也适合用过MySQL但一直没系统梳理过的同学。

1. 先把“增删查改”这个概念拆清楚

1.1 数据库操作其实分三层

很多新手觉得增删查改就四条SQL,其实MySQL的操作对象分成了三层:库、表、数据行。

  • 库(Database):最外层容器,类似电脑里的文件夹。对库的“增删查改”对应CREATE DATABASE、DROP DATABASE、SHOW DATABASES、ALTER DATABASE。
  • 表(Table):存放在库里的结构化容器,定义好列名、类型、约束后才能装数据。对表的“增删查改”对应CREATE TABLE、DROP TABLE、SHOW TABLES、ALTER TABLE。
  • 数据行(Record):表里真正的一行行记录。日常说的“增删查改”绝大多数指这一层,对应INSERT、SELECT、UPDATE、DELETE。

这三层必须分清。比如DROP TABLE是删表,DELETE FROM是删数据,两者后果天差地别:一个把整张表的骨架都拆掉,一个只是清空里面的行。我见过有人写DELETE FROM清零数据后,误以为表也没了,然后把建表脚本弄丢了,最后只能按紧急流程恢复。这种混乱主要就是对操作对象没有概念。

分层理解之后,再看一条SQL就不会慌。你看到DROP,先问自己一句:它后面跟的是DATABASE、TABLE还是普通查询里的语句?对象不同,危险性完全不同。

1.2 DDL和DML的关键区别

SQL语句按功能可以粗略分成DDL和DML两大类,这对理解“增删查改”很重要。

类型典型语句操作对象能否回滚代表性风险
DDLCREATE、ALTER、DROP、TRUNCATE数据库对象(库、表、索引等)执行后隐式提交,基本不能回滚删表、清表、改结构,都是结构级操作
DMLINSERT、UPDATE、DELETE表里的数据行InnoDB引擎下可用事务回滚忘加WHERE会造成全表数据被改/被删

SELECT严格来说属于DQL(数据查询语言),不属于DML,但习惯上把它归入“查”的范畴。为什么强调这个区别?因为很多事故的源头就是把DML当成“可以后悔”的操作,实际上你如果开了自动提交,一条UPDATE出错也是追不回来的。而TRUNCATE虽然是清空数据,但它属于DDL,不能通过事务回滚,这一点后面删除部分我会再展开。

1.3 操作顺序和权限意识

标准流程是:连接数据库 -> 建库 -> 建表 -> 增删改查。听起来像废话,但实操里经常有人颠倒。比如连上MySQL之后先写INSERT,报错“no database selected”,才想起没建库;或者建表时没指定库,结果表建到了默认库里。

更常见的问题是权限。我见过团队新人拿到服务器地址后,直接用root账号连数据库做业务操作,这很危险。MySQL的权限是分库分表级别的,开发环境应该给业务账号只授需要的几个库的增删改查权限,避免它拥有全局管理权限。

提示:不要在业务代码里用root账号连接数据库。这个习惯一旦形成,后续任何一次SQL写漏WHERE条件,都可能造成全局性的数据损坏,而且很难从权限层面兜底。

2. 从零建库建表:字符集和字段设计一次搞定

2.1 连接MySQL的两种常用姿势

要看懂后面的操作,先要能连上MySQL。命令行是最通用的方式:

mysql -h 127.0.0.1 -P 3306 -uroot -p

输入密码后进入交互终端。其中-h指定主机,-P指定端口,-u指定用户名。有些新人会在这里混淆大小写:密码是-p,端口是-P,两个不一样。

图形客户端我用过不少,Navicat和DBeaver都有,DBeaver开源免费,适合练手。不过我还是建议命令行至少要会用,因为很多服务器上是纯命令行环境,没有图形界面。连上以后可以先执行:

SELECT VERSION();

mysql>提示符下会返回当前版本号。MySQL 5.7和8.0的默认认证插件不同,8.0版本在连接工具时可能遇到认证报错,这个放到后面问题排查部分说。

2.2 建库、选字符集和切换库

第一步创建数据库,我建议养成写标准习惯:

CREATE DATABASE IF NOT EXISTS credit_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

IF NOT EXISTS的意思是“如果不存在才创建”,重复执行不报错,这在初始化脚本里非常有用。字符集选utf8mb4而不是utf8,是因为utf8mb4能完整存储中文、生僻字和emoji。utf8是utf8mb4的子集,很多老库用了utf8,插入表情符号时直接报错,换库又很麻烦。

建好之后查看和切换:

SHOW DATABASES; USE credit_db;

SHOW DATABASES像列文件夹,USE是进入某个库。如果建库时忘记指定字符集,可以后面调整:

ALTER DATABASE credit_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

注意,ALTER DATABASE只改库的默认字符集,不会自动改已经建好的表。表有自己的字符集设置,所以建表时也要显式指定,否则可能继承一个错误值。

2.3 设计一张会员积分表

我拿这个场景来跑全流程:假设某个小活动系统需要记录会员积分。先设计一张user_points表,包含用户ID、用户名称、电话、积分、会员等级、创建时间和更新时间。

CREATE TABLE IF NOT EXISTS user_points ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, user_name VARCHAR(50) NOT NULL, phone VARCHAR(20) DEFAULT NULL, points INT NOT NULL DEFAULT 0, level VARCHAR(20) DEFAULT '普通会员', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

几个核心设计点我解释一下:

  • id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY:主键是自增无符号整数。AUTO_INCREMENT让数据库自动生成唯一序号,不需要应用层关心。
  • NOT NULL:这个列不允许为空,比如user_id,没有用户ID的话积分记录没有意义。
  • DEFAULT NULL:允许为空的可选字段,比如电话。
  • DEFAULT 0:积分默认值是0,避免业务代码每次插入都写默认值。
  • DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP:插入时自动写当前时间,更新时自动刷新。这个设计省了应用层不少事,避免每次UPDATE都要手动改updated_at。

2.4 查看和修改表结构的三板斧

表建好后,养成确认结构的习惯:

DESC user_points; SHOW CREATE TABLE user_points; SHOW TABLES;

DESC能看到列名、类型、是否为空、默认值等简要信息。SHOW CREATE TABLE能看到完整的建表语句,包括字符集、引擎、约束,排查问题时很有用。SHOW TABLES看库里有哪些表。

有时候业务加需求,需要改表结构,常用的ALTER写法:

ALTER TABLE user_points ADD COLUMN remark VARCHAR(255) COMMENT '备注'; ALTER TABLE user_points MODIFY COLUMN phone VARCHAR(30); ALTER TABLE user_points DROP COLUMN remark;

ADD是加列,MODIFY是改类型,DROP COLUMN是删列。这里要劝一句:建表阶段尽量设计完整,宁可多花十分钟想清楚字段,也不要上线后频繁ALTER。表数据量小的时候无所谓,数据量大了之后,即使是MySQL 8.0的在线DDL,也可能遇到锁表或性能问题,最好安排在低峰期执行。

3. 新增数据:INSERT的几种写法和防坑细节

3.1 单条插入是最基本动作

表建好之后,往里加数据就用INSERT。最简单的一条:

INSERT INTO user_points (user_id, user_name, phone, points) VALUES (1001, '张三', '13800000000', 120);

这里括号里的列名和后面的值要一一对应。如果省略某些列,例如不写phone,那它会使用默认值NULL;不写points,它就是0。id列我故意没写,因为它是自增主键,数据库会自动生成。回到MySQL命令行,正确的回应是:

Query OK, 1 row affected (0.01 sec)

这表示插入成功。注意INSERT本身不分“成功返回几行”,影响行数是1。如果你写了一句SQL却返回0 row affected,说明可能被忽略或者没匹配到,这种场景等下聊。

3.2 批量插入的效率优势

如果有多条数据要加,千万不要一条条INSERT循环执行。正确做法是一条INSERT带多个VALUES:

INSERT INTO user_points (user_id, user_name, phone, points) VALUES (1002, '李四', '13811111111', 300), (1003, '王五', '13822222222', 500), (1004, '赵六', NULL, 200);

批量插入的好处是明显减少客户端和MySQL服务器之间的网络往返,也减少日志写入次数。我实际测试过,插入一万条数据,循环单条插入和批量插入的耗时能差一个数量级以上。

不过批量也不是越大越好。我习惯每次500到1000条作为一个批次,避免单条SQL报文过大。有些框架自带批量提交配置,也要注意它的batchSize参数,太小没效果,太大可能把数据库内存和网络带宽一下子吃满。

3.3 自增主键、默认值和NULL的边界

上面那张表里id是自增主键,插入时可以完全不管,让引擎分配。但有几种情况值得注意:

-- 显式指定自增主键值 INSERT INTO user_points (id, user_id, user_name, points) VALUES (10000, 1005, '钱七', 80);

比如你想把ID从10000开始,手动指定是可以的,但后面自增计数器会跟着往上跳。自增主键有个特性:即使你指定ID成功了,下一个自增ID不一定是你指定的ID+1,MySQL为了性能会预留一些值。所以不要依赖自增ID的连续性,它本来就只是唯一标识,不是业务序号。

如果希望在某一列使用默认值,也可以显式写DEFAULT:

INSERT INTO user_points (user_id, user_name, points, level) VALUES (1006, '孙八', DEFAULT, DEFAULT);

这里的level会变成'普通会员',points变成0。不过大多数情况下,省略列名更直观。

3.4 插入时的常见报错与对策

新手最容易在插入环节碰到这几个问题:

  • 中文乱码:往表里插中文,结果查出来是???。原因通常是客户端连接字符集和库表字符集不一致。解决办法是执行SET NAMES utf8mb4;,并确保连接工具也设置成utf8mb4。
  • 主键冲突:插入时指定的主键ID已经存在,报错Duplicate entry '1004' for key 'PRIMARY'。
  • NOT NULL约束冲突:给非空列插NULL,报错Column 'user_id' cannot be null。
  • 字段长度超限:varchar(20)里塞了30个字符,报错Data too long for column 'phone'。

主键冲突非常常见,尤其是做同步或重试场景。可以这样处理:

INSERT INTO user_points (id, user_id, user_name, points) VALUES (1001, 1001, '张三', 999) ON DUPLICATE KEY UPDATE points = VALUES(points);

这个语句的意思是:如果主键或唯一索引冲突,就改成更新points字段。注意在MySQL 8.0里,VALUES(points)这种写法已经标记为deprecated,官方建议用别名写法,但不影响基础学习阶段使用。如果只是想“存在就跳过”,可以用INSERT IGNORE,不过要小心它会把其他类型的错误也吞掉,排查问题时会很迷惑。

4. 查询数据:SELECT是日常工作的绝对主角

4.1 从SELECT *开始,但别用它收尾

查询数据最直接的SQL是:

SELECT * FROM user_points;

星号表示返回所有列,所有行。这条语句在练习时没问题,但在生产环境要非常克制。原因很简单:一张表可能有几十个字段,其中可能有长文本字段,业务代码根本不需要;数据量到百万级以后,SELECT *会把大量用不上的数据从磁盘读进内存,再通过网络传输给客户端,白白消耗资源。

我自己的习惯是:只SELECT业务真正需要的字段,比如:

SELECT user_id, user_name, points FROM user_points;

这样查询的意图更清晰,也方便后续在字段上建立联合索引优化。

4.2 WHERE条件过滤、排序和LIMIT分页

查询的核心是WHERE条件。比如查积分大于100的用户:

SELECT user_id, user_name, points FROM user_points WHERE points > 100 ORDER BY points DESC LIMIT 5;

WHERE过滤出符合条件的行,ORDER BY按积分从高到低排序,LIMIT 5只取前5条。我强烈建议任何线上查询都养成加LIMIT的习惯,防止一次扫出几十万行。

WHERE里可以用AND、OR组合条件:

SELECT user_id, user_name, level, points FROM user_points WHERE level = '普通会员' AND points BETWEEN 100 AND 500;

也可以用IN匹配多个值:

SELECT user_id, user_name FROM user_points WHERE user_id IN (1001, 1002, 1003);

还可以用LIKE做模糊查询:

SELECT user_id, user_name FROM user_points WHERE user_name LIKE '李%';

这里有一个重要认知:LIKE的写法如果以通配符开头,例如'%李',MySQL通常无法使用该列上的B-tree索引,会走全表扫描。数据量大时,这种查询很慢。模糊搜索需求如果很常见,建议用专业的全文索引或搜索引擎方案,不要在业务表里硬扛。

4.3 聚合统计和HAVING的准确位置

查询不只拉数据,还会统计。比如按会员等级分组,看每个等级的人数和总积分:

SELECT level, COUNT(*) AS user_count, SUM(points) AS total_points FROM user_points GROUP BY level HAVING COUNT(*) >= 1 ORDER BY total_points DESC;

GROUP BY把相同level的行归组,COUNT(*)统计每组行数,SUM(points)计算总积分。这里要注意HAVING和WHERE的区别:WHERE是在分组前过滤原始行,HAVING是在分组后过滤组结果。如果两者都能用,优先用WHERE,因为它能减少参与分组的数据量。

还有个和MySQL 5.7以上版本相关的坑:如果启用了ONLY_FULL_GROUP_BY模式,SELECT后面的非聚合列必须完全出现在GROUP BY里,或者被聚合函数包裹。比如你SELECT user_id,但GROUP BY level,就会报错。我在帮人排查SQL时经常看到这类问题。

4.4 多表联查先掌握两种JOIN

真实项目很少只在一张表里查,往往需要关联用户表、订单表等。先建一张简化的用户表orders来做演示:

CREATE TABLE orders ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, order_amount DECIMAL(10,2) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

插入几条数据后,联查积分表里的user_id和订单表的订单金额:

SELECT p.user_id, p.user_name, o.order_amount FROM user_points p INNER JOIN orders o ON p.user_id = o.user_id;

INNER JOIN只返回两表能匹配上的行。如果希望保留左表所有用户,即使他没有订单,就用LEFT JOIN:

SELECT p.user_id, p.user_name, o.order_amount FROM user_points p LEFT JOIN orders o ON p.user_id = o.user_id;

基础阶段先掌握这两种就够了。JOIN的关联字段最好都有索引,否则两张大表关联时会做多次全表扫描,慢到难以接受。

4.5 查询性能的几条直觉

数据库优化是个大话题,但入门阶段可以先记住几条直觉:

  • 能用索引就用索引,常见的WHERE字段、ORDER BY字段、JOIN关联字段都值得关注。
  • 避免在大表上频繁执行SELECT COUNT(*),InnoDB的Count操作不像MyISAM那样直接读总数,而是逐行统计。
  • 分页查询不要用OFFSET无限翻页,大数据量下越翻越慢,需要结合业务做游标或条件分页。
  • 写SQL之前先跑一下EXPLAIN SELECT ...,看看key列有没有走索引,rows列估算扫描多少行。这一步能省掉大量线上性能问题。

5. 更新数据:UPDATE的正确姿势与现场还原

5.1 UPDATE的基础语法和影响行数

更新数据用UPDATE,基本结构:

UPDATE user_points SET points = points + 50 WHERE user_id = 1001;

SET后面可以同时更新多个字段,用逗号分隔:

UPDATE user_points SET points = points + 50, level = 'VIP会员' WHERE user_id = 1001;

如果表里设计了updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,那么这个字段会在UPDATE时自动刷新,不需要手动写。这也是我建表时喜欢加它的原因。

执行成功后MySQL返回:

Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0

注意,如果更新后的值和原值一样,通常Changed是0,但Rows matched是1。这不算错误,只是MySQL发现没有实际变化。

5.2 最危险的场景:忘记WHERE条件

UPDATE最经典的事故就是忘记WHERE。看这句:

UPDATE user_points SET points = 0;

它会把user_points表里所有用户的积分全清零。没有WHERE条件,UPDATE默认作用于全表。我见过不止一次:同事在测试环境想重置某一个用户,结果所有会员积分都回归原点,活动当天直接翻车。

怎么防?我给自己立过一个规矩:写UPDATE之前,先用相同WHERE条件写一条SELECT。

-- 先查 SELECT user_id, user_name, points FROM user_points WHERE user_id = 1001; -- 再改 UPDATE user_points SET points = 0 WHERE user_id = 1001;

这样能在执行前看到即将影响哪些行,心理有个底。另外,生产环境的核心表最好做变更规范,UPDATE和DELETE语句必须经过同事或审核工具确认。

如果已经身处一个不支持回滚的自动提交会话里,这句UPDATE后悔药都没得吃。所以批量操作前开显式事务是我的另一个铁律:

START TRANSACTION; UPDATE user_points SET points = points + 100 WHERE user_id IN (1001, 1002); -- 检查影响行数,确认无误 COMMIT; -- 如果不对,执行 ROLLBACK;

用事务把这个操作包起来,确认影响行数符合预期再提交。测试环境更要养成这个习惯,错了就ROLLBACK,精力成本低很多。

5.3 大批量更新要分批次

有时候业务需要给所有用户统一加积分:

UPDATE user_points SET points = points + 100;

这句在数据量小的时候没问题,但表里几十万行甚至更多时,一条UPDATE会长时间持有大量行锁,可能导致业务阻塞和主从复制延迟。更稳的方案是分批更新:

UPDATE user_points SET points = points + 100 WHERE id BETWEEN 1 AND 1000; -- 循环 UPDATE user_points SET points = points + 100 WHERE id BETWEEN 1001 AND 2000;

每批数量控制在1000到5000之间,批与批之间可以加个短暂sleep,给数据库一个喘息窗口。这样单次锁范围小,对其他查询的影响可控。

5.4 UPDATE JOIN能做什么

有时候需要根据另一张表的值来更新当前表。比如根据orders表的总消费金额更新会员等级,可以这样写:

UPDATE user_points p JOIN ( SELECT user_id, SUM(order_amount) AS total_amount FROM orders GROUP BY user_id ) o ON p.user_id = o.user_id SET p.level = CASE WHEN o.total_amount > 1000 THEN '钻石会员' ELSE '普通会员' END;

这里用子查询先算出每个用户的总消费,再关联更新等级。这类写法能力很强,但可读性稍差,新手阶段建议先在测试表上验证。改数据前保留好备份,始终是头等大事。

6. 删除数据:DELETE、TRUNCATE、DROP三个兄弟的区别

6.1 DELETE按条件删除数据行

删除数据最常用的是DELETE:

DELETE FROM user_points WHERE user_id = 1004;

这条语句会删除user_id等于1004的行,表结构和其余行不受影响。如果WHERE条件省略:

DELETE FROM user_points;

这会把表里所有行删光,但表还在,自增计数一般不会重置。这条语句因为属于DML,在InnoDB引擎下可以用事务包住,误操作后还能ROLLBACK:

START TRANSACTION; DELETE FROM user_points; SELECT COUNT(*) FROM user_points; -- 发现删光了 ROLLBACK;

触发ROLLBACK后数据恢复原样。这是我经常用来演示“DELETE还有后悔机会”的例子。但如果你用的是TRUNCATE,结果就不一样了。

6.2 TRUNCATE清空表并重置自增

清空表可以用:

TRUNCATE TABLE user_points;

TRUNCATE是DDL,它会把所有行删掉,并且重置自增计数器,一般也会释放存储空间。但它有两个特点需要注意:第一,不能加WHERE条件,一跑就是全表;第二,它属于DDL,执行时隐式提交,事务内无法回滚。

所以TRUNCATE适合清理临时表、测试表或者准备重新导入数据的表。如果你只是想“清空线上业务的流水明细”,先再三确认这是不是业务真正想要的,并且备份存在。

6.3 DROP删除表结构

DROP是更彻底的操作:

DROP TABLE user_points;

表的结构、数据、索引、约束全部消失,连后悔的机会都没有。DROP后面跟DATABASE,就是整个库都没了:

DROP DATABASE credit_db;

这个命令平时基本不会出现在业务代码里,只有初始化脚本或彻底下线项目时才用。我给新人的建议是:不到万不得已,不要在线上环境敲DROP。如果必须做,先确保有可恢复的备份,并且有专人复核。

三兄弟的区别我经常用一张表说明:

操作属于能否加WHERE能否事务回滚自增ID表结构
DELETEDML可以可以不重置保留
TRUNCATEDDL不可以不可以重置保留
DROPDDL不可以不可以彻底消失删除

6.4 业务系统优先考虑逻辑删除

虽然这篇文章讲的是物理增删查改,但真实业务系统里,核心数据很少直接物理删除。更常见的做法是加一个is_deleted字段,值为0表示正常,1表示已删除。查询时统一过滤:

SELECT user_id, user_name, points FROM user_points WHERE is_deleted = 0;

这样做的好处很多:防止误删无法恢复,保留历史数据方便审计,也避免其他表的外键被破坏。我参与过的几个项目,用户注销账号都不是直接DELETE用户行,而是把用户状态改成注销,保留基本信息备查。只有在月度归档、清理无效数据时,才会物理删除。

6.5 误删之后,唯一的救星是备份

如果真发生误删,而且没有事务可以回滚,能从灾难里救你的只有备份。MySQL最常用的备份工具是mysqldump:

mysqldump -uroot -p credit_db > credit_db_backup.sql

恢复时执行:

mysql -uroot -p credit_db < credit_db_backup.sql

注意mysqldump备份的是某个时间点的快照,备份之后新增的数据仍然会丢。所以生产环境通常还要配binlog做增量恢复,这是更高阶的话题。但基础认知必须建立:做任何危险操作前,先备份;越重要的数据,备份频率越高。

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

7.1 连接不上:ERROR 2003

MySQL客户端连接服务器报ERROR 2003 (HY000): Can't connect to MySQL server on '127.0.0.1',常见原因有:MySQL服务没启动、监听端口不是3306、防火墙拦截。

排查思路可以按顺序来:先看服务状态,Linux上执行systemctl status mysql或service mysql status;再确认端口存在,执行netstat -tlnp | grep 3306;接着用telnet 127.0.0.1 3306测试端口通不通。注意MySQL本机连接有时候会用socket文件,命令行加-h 127.0.0.1通常强制走TCP,方便排查。

7.2 中文乱码

插入中文后查询显示问号,主要问题是字符集链路不一致。库表字符集、连接字符集、客户端显示字符集,三个环节都要设置成utf8mb4。最简单做法是连接MySQL后先执行:

SET NAMES utf8mb4;

同时建库建表时显式指定DEFAULT CHARSET=utf8mb4,图形客户端里也要把连接编码改成UTF-8。我遇到过一个情况:表是utf8mb4,客户端也是UTF-8,但通过某个老接口写入的数据还是乱码,后来发现是那个接口所在程序的MySQL JDBC连接串没显式指定characterEncoding=utf8。

7.3 Duplicate entry:主键和唯一键冲突

插入数据时报Duplicate entry '1004' for key 'PRIMARY',说明主键值已存在。如果业务允许覆盖,用:

INSERT INTO user_points (id, user_id, user_name, points) VALUES (1004, 1004, '赵六', 999) ON DUPLICATE KEY UPDATE points = 999;

如果是唯一索引也同理。但要注意,ON DUPLICATE KEY UPDATE会占用自增ID,即使触发了更新,自增也可能跳号。不要用自增ID是否连续来判断数据是否正常。

7.4 UPDATE或DELETE影响行数为0

执行更新或删除,返回0 row affected,常见原因:

  • WHERE条件没有匹配到任何行,最简单也最常见。
  • 字符串列用数字去匹配,MySQL会做隐式类型转换,可能导致匹配失败。
  • 更新前后的值一样,比如把点数从100改成100,MySQL返回0 affected。
  • 当前事务隔离级别下,数据还没提交,另一个会话看不到。MySQL默认是REPEATABLE READ,事务隔离会让未提交的数据在别的事务里不可见。

排查方法很直接:先用相同WHERE条件跑SELECT COUNT(*)。如果SELECT查到了但UPDATE没影响,再检查是不是有锁等待或类型转换问题。

7.5 DELETE后磁盘空间没下降

经常有朋友问:我删掉了几十万行数据,为什么磁盘空间没变化?原因很简单:InnoDB的DELETE是逻辑删除,数据文件不会立即收缩。要回收空间,通常需要重建表或执行优化操作,但生产环境要谨慎。

测试环境可以用:

OPTIMIZE TABLE user_points;

它会重建表并整理碎片,但同时可能锁表或消耗大量资源,不要在业务高峰期对核心大表执行。TRUNCATE通常会释放表空间,因为它直接重建了表结构文件。

7.6 Access denied:权限不足

执行建表或数据操作时看到ACCESS DENIED for user 'app_user'@'localhost',这是数据库权限问题。开发环境可以单独给业务账号授需要的权限:

GRANT SELECT, INSERT, UPDATE, DELETE ON credit_db.* TO 'app_user'@'localhost'; FLUSH PRIVILEGES;

credit_db.*表示credit_db库下所有表。如果只授某一张表,可以写成credit_db.user_points。注意MySQL账号权限修改后,新连接才生效;FLUSH PRIVILEGES通常用于手动刷新权限表,实际场景中还要结合账号配置来理解。

7.7 语法错误和分号问题

SQL写错最常见的是漏分号、字符串引号不配对、关键字拼写错误。比如UPDAT少个E,MySQL会提示语法错误。这类报错看提示位置通常能定位。另一个不太显眼的问题是字符串内容里的单引号,如果用户名字叫“O'Brien”,SQL里就要用两个单引号转义:

INSERT INTO user_points (user_name) VALUES ('O''Brien');

缺少转义会直接导致语法报错,看起来是SQL错了,实际是引号没处理。

8. 写在最后:我踩过的坑和几条实操体会

MySQL的增删查改语法并不难,难的是每次操作前都保持对数据的敬畏。我在实际项目里见过几次大事故,共同点都是少写了WHERE。后来我给自己定了几个很笨但有效的规矩:凡是UPDATE或DELETE,第一句先写SELECT核对影响范围;凡是批量操作,先开事务,确认无误再COMMIT;凡是清空或删表,必须先确认备份存在。另外,建表时尽量把字符集、约束、默认值都想清楚,别指望事后频繁ALTER。

最后分享一个练习建议:自己搭一个零成本的MySQL环境,建一张类似user_points的练习表,把INSERT、SELECT、UPDATE、DELETE全部敲一遍,再故意制造几种报错,比如主键冲突、非空约束冲突、字符集乱码。多踩几次坑,比看十遍文档都管用。希望这篇文章能帮你在“增删查改”这条路上少走一些弯路。

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

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

立即咨询