MySQL 表的基本操作,听起来像是教科书里最不起眼的一章,但真正干过几年开发的人都知道,线上事故十有八九都出在这几个最基础的动作上:建表时犹豫了一个字段类型,删数据时少写了一个条件,改表结构时没留意算法。这篇文章不打算讲炫技的调优,就把表相关的增删改和结构变更掰开揉碎,说清楚每一步怎么选、为什么这么选,以及我实际踩过的坑。适合刚入门 MySQL 的学习者,也适合工作了两三年、一直靠模板建表却没系统捋过一遍的开发者。
1. 建表之前,先把字段类型和约束想明白
1.1 字段类型选错,后期改动成本超乎想象
很多新手建表时的心态是“先随便建,后面再改”,这个想法在 MySQL 里代价很大。改字段类型属于 DDL 操作,一旦表里数据量上来了,ALTER TABLE 可能要重建整张表,这个过程不仅慢,还会长时间锁住写入请求。所以我一直强调,建表时花十分钟把类型想清楚,比事后加班修数据划算得多。
先看整数类型。INT 占 4 字节,范围大约在 -21 亿到 21 亿之间,单表数据量到千万级时,自增主键是很可能撑满这个范围的。一旦 INT 溢出,插入就会直接报错,到时候再改成 BIGINT,在几百万行的大表上执行 ALTER,代价非常大。所以现在新业务主键我基本默认 BIGINT,8 字节换来的是不用焦虑上限。如果你拿不准,直接 BIGINT 不会错,尤其互联网业务增长不可控,别为了省那点磁盘空间给自己埋雷。
再说字符串。VARCHAR 和 CHAR 的选择逻辑不复杂:长度变化大用 VARCHAR,固定长度用 CHAR。身份证号、手机号这种定长字段,用 CHAR 可以避免变长字段的额外开销。但要注意一个细节,VARCHAR 括号里的数字是字符数,不是字节数,在 utf8mb4 字符集下,一个汉字占 4 字节。另一个容易忽略的问题是 VARCHAR 的最大长度,当行长度超过一定阈值后,InnoDB 会把过长的字段放到溢出页存储,这会对查询产生额外 IO。很多初级开发者习惯把所有字符串都写成 VARCHAR(255),后来需要存更长的文本时再改表,得不偿失。我的建议是:名称、标题这类短字段用 VARCHAR(64) 或 VARCHAR(128),拿不准长度的内容再放宽,但一定要清楚这是有代价的。
小数类型是另一个重灾区。金额、汇率、评分这种精确小数,必须用 DECIMAL,不要用 FLOAT 或 DOUBLE。二进制浮点数在计算和比较时会有精度误差,比如 0.1 + 0.2 的结果不是精确的 0.3,这在金额计算里是致命的。DECIMAL(10,2) 表示总位数 10 位、小数点后 2 位,可以按业务量级调整。日期时间类型我统一建议 DATETIME,TIMESTAMP 到了 2038 年就超出上限了,而且它存储时会根据数据库时区做换算,很容易出现“看到的时间和实际时间对不上”的怪异问题。DATETIME 虽然多占 4 字节,但直观、稳定、无歧义。
| 类型 | 占用空间 | 适用场景 | 注意事项 |
|---|---|---|---|
| INT | 4 字节 | 枚举、状态、中量数据 ID | 千万级主键慎用,建议直接 BIGINT |
| BIGINT | 8 字节 | 主键、订单号等大数值 | 默认推荐 |
| VARCHAR(n) | 变长 | 标题、名称、描述 | n 是字符数,utf8mb4 下汉字占 4 字节 |
| CHAR(n) | 定长 | 手机号、身份证号 | 尾部空格会被去掉 |
| DECIMAL(m,d) | 变长 | 金额、精确小数 | 不要用 FLOAT/DOUBLE |
| DATETIME | 8 字节 | 创建时间、更新时间 | 无时区问题 |
| TIMESTAMP | 4 字节 | 兼容老系统 | 2038 年上限,有时区换算问题 |
| TEXT/BLOB | 变长 | 长文本、文件内容 | 避免直接 SELECT *,会拖垮性能 |
1.2 约束不是摆设,是真能救命的
约束是数据库保证数据质量的最后一道闸门,但很多人建表时图省事,能省就省,结果脏数据进来后查也查不动、改也不敢改。
主键是 InnoDB 的聚簇索引,没有主键时 InnoDB 会生成一个隐藏的 rowid,这会导致主从复制和数据定位都很麻烦。主键设计上我强烈建议用自增 BIGINT,不要用 UUID 或雪花 ID 做主键。原因在于 InnoDB 的索引结构是 B+ 树,自增主键新插入的数据总是在索引末尾追加,顺序写性能很好;UUID 是随机的,新行可能插入到 B+ 树中间任意位置,频繁触发页分裂,产生大量随机 IO,写入性能下降非常明显。
NOT NULL 和 DEFAULT 这两个约束看着不起眼,但对查询的影响非常大。逻辑上不等于 NULL,很多人在 WHERE 条件里写 name != 'a',结果 NULL 的行就是不显示,因为对 NULL 做比较返回的不是真也不是假,而是“未知”。这种问题排查起来非常隐蔽。所以业务字段能非空就非空,拿不到值就给 DEFAULT,比如状态字段 DEFAULT 0、创建时间 DEFAULT CURRENT_TIMESTAMP。UNIQUE KEY 用于防止重复数据,比如用户表的手机号,你在应用层判断一次,不如数据库直接加唯一索引,双保险。
外键我建议业务量大后尽量少用。MySQL 的外键在每次插入和删除时都要做一致性检查,锁范围大,多表关联时容易成为性能瓶颈。现代架构里,数据一致性往往由应用层事务或最终一致性方案来保证,外键更多的是在面试题里出现。不过如果你维护的是企业内部系统,数据量不大,外键还是可以用的,它有数据库层面的强约束,能省掉很多应用层判断。另外 CHECK 约束在 MySQL 8.0.16 之前只是语法兼容,不真正生效,升级到 8.0.16 之后才真正支持,低版本上别依赖它。
1.3 字符集和排序规则,建表时顺手就定了
字符集这个坑,几乎每个团队都踩过。早期很多 MySQL 实例默认字符集是 latin1 或 utf8,导致存 emoji 表情时直接报错或者变成问号。这里的 utf8 实际上是 utf8mb3,最多只能存 3 字节,而 emoji 表情是 4 字节,只有 utf8mb4 才是真正的完整 UTF-8。所以建表时我默认全部用 utf8mb4,字符排序规则用 utf8mb4_0900_ai_ci(MySQL 8.0 默认)或 utf8mb4_general_ci(5.7 常用),一个字母都不要错。
排序规则直接决定了字符串比较、排序、去重时是否区分大小写、是否忽略重音。ai 表示 accent insensitive,ci 表示 case insensitive,也就是默认不区分重音和大小写。如果业务上需要精确区分大小写,要用 utf8mb4_bin。这里有个很实际的问题:如果一张表的字符集和另一张表不一致,做 JOIN 时 MySQL 会报 Illegal mix of collations 错误,排查起来很头疼。所以建库的时候就把默认字符集定成 utf8mb4,后面建表不写字符集也能继承,这是最省心的方案。
一个完整的建表语句长这样:
sql复制CREATE TABLE user_info (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
mobile VARCHAR(20) NOT NULL COMMENT '手机号',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0禁用 1启用',
balance DECIMAL(10,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_mobile (mobile),
KEY idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表';
这里有几个细节:id 用了 BIGINT UNSIGNED,可以充分利用正数范围;mobile 加唯一索引,保证业务上不重复;update_time 用了 ON UPDATE CURRENT_TIMESTAMP,每次更新行时自动刷新,省得应用层再手动维护。ENGINE 显式写成 InnoDB,虽然 8.0 默认就是 InnoDB,但显式写出来能防止未来实例默认引擎被改掉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 增删改:每天用得最多的三类操作
2.1 INSERT 插入数据的几种姿势与选择
INSERT 是平时最常用的语句,但很多人不知道它有几种写法,更不知道不同写法的适用场景。
最基础的是指定字段名插入:
sql复制INSERT INTO user_info (username, mobile, status) VALUES ('张三', '13800001111', 1);
强烈建议写 INSERT 时把字段名列出来,不要用 INSERT INTO user_info VALUES (...)。如果不写字段名,SQL 就必须严格按表定义的字段顺序来,一旦表结构加了列或调整了顺序,这条 SQL 就报废了。显式列出字段名还有一个好处,SQL 的可读性更强,别人接你的代码时一眼就能看出每个值对应哪个字段。
批量插入时,要用一条语句带多组 VALUES,而不是在应用层循环几百次执行单条 INSERT:
sql复制INSERT INTO user_info (username, mobile, status) VALUES
('张三', '13800001111', 1),
('李四', '13800002222', 1),
('王五', '13800003333', 0);
一条多 VALUES 语句和多次单条 INSERT 相比,少了大量网络往返和 SQL 解析开销,插入速度能提升好几倍。但也要注意一条 SQL 不要塞太多行,建议每批 500 到 1000 行,太多的话会撑大 binlog,还可能超过 max_allowed_packet 限制。
MySQL 还支持一种特殊语法 INSERT ... SET:
sql复制INSERT INTO user_info SET username = '赵六', mobile = '13800004444', status = 0;
这种写法不是标准 SQL,但 MySQL 和 MariaDB 都支持。它的好处是字段和值挨在一起,视觉上不易串位,适合插入字段比较少的场景。但要注意 SET 写法在批量插入时不适用,只能一条一条来。
关于重复键处理,这是面试和实际开发都常碰到的场景。INSERT ... ON DUPLICATE KEY UPDATE 可以在唯一键冲突时更新这条记录:
sql复制INSERT INTO user_info (username, mobile, status) VALUES ('张三', '13800001111', 1)
ON DUPLICATE KEY UPDATE status = VALUES(status);
这个方法依赖唯一索引或主键来判断是否冲突。它的含义是:如果 mobile 这个唯一键已经存在,就把 status 更新为新值。这个写法很适合做幂等写入,避免每次插入前先 SELECT 查一遍。注意 VALUES() 函数在 MySQL 8.0.20 开始被标记为废弃,官方推荐改用别名语法:
sql复制INSERT INTO user_info (username, mobile, status) VALUES ('张三', '13800001111', 1) AS new
ON DUPLICATE KEY UPDATE status = new.status;
如果你是从旧版本升上来的,建议尽快适应新写法。
还有一种 INSERT ... SELECT 的用法,从一个表查询结果直接插入另一个表:
sql复制INSERT INTO user_info_backup (username, mobile, status)
SELECT username, mobile, status FROM user_info WHERE create_time < '2023-01-01';
这在做数据归档、历史数据迁移时非常实用。但要注意,INSERT ... SELECT 在默认隔离级别下会给源表加共享锁,大表上操作可能阻塞源表写入,建议在业务低峰期执行,或者分批做。
2.2 UPDATE 更新数据:WHERE 是底线,影响行数是关键
UPDATE 是所有 DML 里最需要敬畏的语句。新手最常犯的错误,就是写 UPDATE 忘了 WHERE,结果把整张表的数据全部改掉。我见过不止一次线上事故,原因就是一条原本只想改某一个人的 SQL,忘记带条件,全表状态被置为 0,最后只能靠备份恢复。
UPDATE 的基本语法很简单:
sql复制UPDATE user_info SET status = 1 WHERE mobile = '13800001111';
执行之前,我建议先跑一条 SELECT,确认 WHERE 条件命中的数据是不是预期的那几条:
sql复制SELECT id, username, status FROM user_info WHERE mobile = '13800001111';
确认无误,再执行 UPDATE。这两个动作虽然看起来多了一步,但能挡住绝大多数误操作。MySQL 客户端执行 UPDATE 之后会返回 Rows matched 和 Changed 这两行信息,Rows matched 是条件命中的行数,Changed 是实际被修改的行数;如果两端数字差异很大,就要想想是不是条件写得不对。
UPDATE 还支持多表关联更新,这在业务开发中经常用到:
sql复制UPDATE user_info u
INNER JOIN order_info o ON u.id = o.user_id
SET u.status = 1
WHERE o.order_no = '202401010001';
这个写法在写复杂的业务逻辑时能省不少事,但要注意多表 UPDATE 里的 JOIN 可能会锁住多张表的相关行,如果表很大或并发高,尽量改成两条简单的 UPDATE 分步执行。
还有一点非常重要:在高并发业务里,UPDATE 一定要加合理的索引条件,否则会全表扫描加行锁,甚至升级为表锁。比如按 mobile 更新时,如果 mobile 没有索引,MySQL 只能全表扫一遍找到匹配的行,这个过程会把很多行都锁住,产生严重的锁等待。所以 WHERE 条件的字段必须建索引,这不仅是查询性能问题,也是锁粒度问题。
关于 LIMIT,MySQL 的 UPDATE 语法支持 LIMIT:
sql复制UPDATE user_info SET status = 1 WHERE status = 0 LIMIT 100;
这在批量状态流转场景里很有用,比如每次只处理 100 条,避免一次锁太多行。不过要注意,UPDATE 加了 LIMIT 后,被更新的是哪 100 条是不可预测的,除非 ORDER BY 指定顺序。所以更稳妥的做法是先把主键查出来,再按主键批量更新。
2.3 DELETE 删除数据:三思而后行
DELETE 是比 UPDATE 更需要谨慎的操作,因为一旦删除,数据就直接从表里消失了。
基本语法:
sql复制DELETE FROM user_info WHERE id = 123;
和 UPDATE 一样,WHERE 是必须的,不带 WHERE 就是清空整张表。很多团队有血泪教训,一条 DELETE 忘带 WHERE,数据全没,然后开始漫长恢复流程。建议在重要表上执行 DELETE 之前,先在测试环境把一条语句的 WHERE 条件跑一遍,确认命中范围,再切到生产执行。
DELETE 和 TRUNCATE 经常被拿来对比,它们虽然都是删除数据,但底层逻辑完全不一样。
| 对比项 | DELETE | TRUNCATE |
|---|---|---|
| SQL 类型 | DML | DDL |
| 是否可以带 WHERE | 可以 | 不可以 |
| 是否可回滚 | 在事务中可回滚 | 隐式提交,不可回滚 |
| 是否重置自增 | 不重置 | 重置为初始值 |
| 是否释放表空间 | 不释放,只是标记删除 | 直接重建表,释放空间 |
| 执行效率 | 逐行删除,慢 | 极快 |
| 触发触发器 | 会触发 | 不触发 |
很多人以为 TRUNCATE 在事务里也能回滚,这是错的。TRUNCATE 执行时会在事务里做隐式提交,即使后续 ROLLBACK 也没有数据回来。所以 TRUNCATE 的表,几乎只有靠备份才能救回数据。
DELETE 只标记删除,记录还在数据页里,所以表空间不会缩小。如果删除了大量数据又需要物理回收空间,可以用 OPTIMIZE TABLE 或 ALTER TABLE ... FORCE 来重建表,但这两个操作在大表上会锁表,必须在低峰期做。
大量删除还有一个实践问题:一次性 DELETE 几百万行,会撑大 binlog,拉长同步延迟,还可能把 undo log 撑爆。正确做法是分批删除:
sql复制DELETE FROM log_info WHERE create_time < '2024-01-01' LIMIT 1000;
反复执行这条语句,直到删除完成。每条 SQL 不要删太多,一般 1000 行一批比较稳妥,每批之间可以 sleep 几秒,给主从同步留出时间。
3. 改表结构:ALTER TABLE 的正确打开方式
3.1 加字段、改类型、改列名的具体写法
项目上线后加需求、改逻辑,几乎每天都在发生。所以 ALTER TABLE 是必备技能,而且要比建表更小心,因为这是直接在可能已经跑了几百万行的表上做手术。
最常用的场景是加字段:
sql复制ALTER TABLE user_info ADD COLUMN age INT NOT NULL DEFAULT 0 COMMENT '年龄';
新加的字段如果允许为空,默认就是 NULL;如果业务上想让它非空,就必须给 DEFAULT,否则已有行的这个字段会被填成 NULL,可能不符合预期。字段位置想调整的话,可以加 AFTER:
sql复制ALTER TABLE user_info ADD COLUMN age INT NOT NULL DEFAULT 0 AFTER birth_date;
把新字段放在某个字段后面,这只是物理存储上的展示顺序,不影响查询性能。除非强迫症,一般不用管,加在最后就行。
修改字段类型用 MODIFY:
sql复制ALTER TABLE user_info MODIFY COLUMN username VARCHAR(128) NOT NULL COMMENT '用户名';
这里要特别注意,MODIFY 会重新定义字段的所有属性,你只写了部分属性时,其他属性会按默认值重置。比如原字段是 VARCHAR(64) NOT NULL,你写 MODIFY COLUMN username VARCHAR(128),那 NOT NULL 可能会丢。所以 MODIFY 时要把所有需要的属性完整写出来,不要图省事。
修改列名用 CHANGE,它比 MODIFY 多一个旧名字参数:
sql复制ALTER TABLE user_info CHANGE COLUMN birth_date birthday DATE NOT NULL;
CHANGE 的语法是 CHANGE 旧列名 新列名 类型 属性,即使只是改名字,也要把类型完整写一遍,不能省略。这个写法经常被吐槽,但就是这么设计的。
删除字段:
sql复制ALTER TABLE user_info DROP COLUMN age;
删除字段会连带数据一起丢失,不可恢复,执行之前一定要确认这个字段真的没用了。如果有任何历史报表或存储过程还在引用它,删掉后线上就会报错。建议先全代码仓库搜一下这个字段的引用情况,再做删除。
加索引也是 ALTER 的常见操作:
sql复制ALTER TABLE user_info ADD INDEX idx_status (status);
ALTER TABLE user_info ADD UNIQUE KEY uk_username (username);
索引的命名规范推荐 idx_字段名 或 uk_字段名,方便 DBA 在排查慢 SQL 时快速识别。加了索引不代表一定被用到,MySQL 优化器会根据统计信息自己判断,这个另说。
3.2 在线 DDL:大表改结构必须懂的原理
很多运维事故都发生在 ALTER TABLE 上。早期 MySQL 版本执行 ALTER,采用的是 COPY 方式,流程是:创建一张新表,把旧表数据逐行拷贝进去,再到新表上建索引,然后把表切换过来。整个过程旧表只能读不能写,或者干脆完全锁表。如果表有几百 GB,这个操作可能执行几个小时,期间业务写入全部卡死,线上事故就这么来的。
MySQL 5.6 开始引入了在线 DDL,后面 5.7、8.0 持续增强。现在 ALTER TABLE 可以指定算法:
- ALGORITHM=COPY:最原始的方式,拷贝数据,锁表,能不用就不用。
- ALGORITHM=INPLACE:原地修改,大部分操作期间允许读写,但具体还要看操作类型。
- ALGORITHM=INSTANT:8.0 开始支持,只修改数据字典,不重建表,几乎瞬间完成。
加字段在 8.0 里默认可能走 INSTANT,所以很多时候你加一个字段,发现执行得飞快,就是这个原因。但不是所有操作都支持 INSTANT,比如修改字段类型、删除字段、加索引,可能要 INPLACE 或 COPY。可以在执行时显式指定:
sql复制ALTER TABLE user_info ADD COLUMN age INT NOT NULL DEFAULT 0, ALGORITHM=INSTANT, LOCK=NONE;
LOCK=NONE 表示允许并发 DML,这是最理想的情况。如果操作不满足 LOCK=NONE 要求,MySQL 会直接报错而不是默默降级成锁表,这是 MySQL 的保护机制。如果想知道一条 ALTER 到底会怎么执行,可以先 EXPLAIN 一下,看它提示的算法和锁级别。
大表上加索引也是高频操作,比如:
sql复制ALTER TABLE user_info ADD INDEX idx_create_time (create_time);
在 5.6 之后,加索引走 INPLACE,允许并发读写,不会锁住整张表。但要提醒的是,即使允许并发,加索引在几亿行的大表上也会跑很久,消耗大量 IO 和 CPU,最好还是在业务低峰期做。如果表是在太大了,可以借助 gh-ost 或 pt-online-schema-change 这类工具,它们通过在影子表上同步增量变更来减少对线上业务的影响,这是大型系统改表结构的标准方案。
3.3 表的重命名、复制与临时表
改表名是小事,但有讲究。直接写:
sql复制RENAME TABLE user_info TO user_info_bak;
RENAME TABLE 是原子操作,执行完要么成功要么失败,不会出现表名指到一半的状态。它还可以一次改多张表,并且是原子的:
sql复制RENAME TABLE user_info TO user_info_bak, user_info_new TO user_info;
这条语句实现了一个经典的“平滑替换表”操作:先把旧表改名备份,再把新表改成正式表名,整个过程应用层的连接不会看到中间状态,非常适合做表结构变更和数据迁移。
复制表结构可以用 CREATE TABLE LIKE:
sql复制CREATE TABLE user_info_bak LIKE user_info;
LIKE 方式会完整复制表结构、索引、默认值,但不复制数据。这是做测试表、临时表最常用的方式。如果想要结构加数据一起复制,可以用 CREATE TABLE AS SELECT:
sql复制CREATE TABLE user_info_bak AS SELECT * FROM user_info;
但这里有个非常隐蔽的坑:AS SELECT 只复制数据,索引、默认值、自增属性、约束全部丢失。你得到的就是一张裸表,只有字段和数据。很多人用它做备份,结果真正要恢复时才发现索引全没了,查询慢到怀疑人生。正确做法是先用 LIKE 复制结构,再 INSERT INTO ... SELECT 导入数据:
sql复制CREATE TABLE user_info_bak LIKE user_info;
INSERT INTO user_info_bak SELECT * FROM user_info;
这样结构和数据都在,索引也在。
临时表是另一个常用工具:
sql复制CREATE TEMPORARY TABLE tmp_user_rank (
user_id BIGINT NOT NULL,
score INT NOT NULL,
PRIMARY KEY (user_id)
) ENGINE=InnoDB;
TEMPORARY 表只在当前会话可见,连接断开后自动删除。它很适合存复杂计算的中间结果,避免把临时数据放在业务表里。注意临时表不要和正式表同名,虽然 MySQL 允许,但会话内访问时同名正式表会被屏蔽,容易出混淆问题。
4. 表操作高频问题与排查思路
4.1 日常排查表状态的几个命令
工作中查表结构、查索引、查状态,有几个命令要烂熟于心。
查看表结构,最完整的信息用 SHOW CREATE TABLE:
sql复制SHOW CREATE TABLE user_info\G
它会输出完整建表语句,包括字符集、引擎、索引、注释,精确到每一个属性。这是排查表结构问题时的第一选择。快速查看字段用 DESC:
sql复制DESC user_info;
DESC 只列出字段名、类型、是否为空、默认值、是否主键等基本信息,没有索引和表级信息。查看索引用:
sql复制SHOW INDEX FROM user_info;
这个是排查慢查询的关键,能看出哪些字段有索引、索引类型、是否唯一、基数是多少。Cardinality 表示索引的区分度,如果很小,说明这个字段重复值很多,索引作用不大。
看表当前状态和数据量,可以查 information_schema:
sql复制SELECT TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, CREATE_TIME, UPDATE_TIME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'user_info';
要注意,InnoDB 的 TABLE_ROWS 是一个估算值,不是精确行数,尤其在大表上偏差可能很大。想精确统计行数只能 COUNT(),但 COUNT() 在 InnoDB 大表上会扫描全表,非常慢。生产环境别随便在大表上跑 COUNT(*),尤其是几亿行的大表。
4.2 误删了数据怎么办
误删数据这件事,每个 DBA 和开发者都想远离,但一旦发生,至少要知道还能怎么救。
先明确前提:如果 MySQL 开启了 binlog 且 binlog_format 是 ROW,那么每秒的数据变更都会被记录。恢复的整体思路是:用最近一次全量备份恢复到备份时刻,再把备份时刻到误操作之前的 binlog 增量日志重放回去,就能把数据还原到误操作发生前的状态。
比如全量备份是凌晨 2 点做的,你在上午 10 点误删了一张表。那么恢复步骤是:先恢复到凌晨 2 点的备份,然后用 mysqlbinlog 解析从 2 点到 10 点之间的 binlog,过滤出误操作之前的所有 SQL,重新执行一遍,最终得到 10 点误删前的数据快照。
这里有个关键点:binlog 不是默认必开的,很多本地开发环境为了省性能会关掉 binlog,一旦误删就只能认栽。所以生产库必须开 binlog,这是数据安全的最后一道防线。另外,binlog 更不要设置超短的过期时间,否则日志被清理了,想恢复也无从谈起。
运维层面,想要防止这类事故,团队可以开启 sql_safe_updates:
sql复制SET sql_safe_updates = 1;
开启后,不带 WHERE 条件的 UPDATE 和 DELETE 会被 MySQL 拒绝执行,相当于强制要求每条 UPDATE/DELETE 都必须有明确的条件。这个设置对防止全表误更新非常有效,缺点是会有一些本意就是想全表更新的业务 SQL 被拦住,需要开发人员显式加条件或用 LIMIT 规避。但我觉得这个牺牲完全值得。
还有一个习惯问题:删除重要数据之前,先把它备份到一张临时表:
sql复制CREATE TABLE user_info_deleted LIKE user_info;
INSERT INTO user_info_deleted SELECT * FROM user_info WHERE id IN (1,2,3);
DELETE FROM user_info WHERE id IN (1,2,3);
这套操作看着多,但对于核心业务表,多这几步能让你半夜安心睡觉。
4.3 面试官常问的几个表操作知识点
表的基本操作看起来基础,面试时却总能翻出花样。这里把高频考点集中梳理一遍。
DELETE、TRUNCATE、DROP 的区别
这是最常见的送分题,但很多人讲不完整。DELETE 是 DML,只删数据不删结构,可以带 WHERE,可以配合事务回滚,不重置自增;TRUNCATE 是 DDL,删数据不删结构,不能带 WHERE,隐式提交不可回滚,重置自增;DROP 是 DDL,把整个表结构连同数据一起删除,连表都没了。一句话总结:DELETE 删行、TRUNCATE 清表、DROP 毁库。
CHAR 和 VARCHAR 的区别
CHAR 是定长字符串,存取速度稍快,会去掉尾部空格;VARCHAR 是变长字符串,按实际内容长度存,占空间更小。面试时再补充一点:CHAR 最大 255 字符,VARCHAR 理论最大可以到 65535 字节,但实际会受行大小和字符集影响,utf8mb4 下实际能存更少。这个细节能让你和背答案的候选人拉开差距。
为什么主键不要用 UUID 而用自增
原因是 InnoDB 的主键是聚簇索引,数据最终按主键顺序物理存储。自增主键插入永远是追加,不会移动已有数据;UUID 主键无序,插入时经常触发页分裂,产生碎片和随机 IO,性能下降明显。另外 UUID 是 36 个字符的字符串,主键占用空间大,每个二级索引都会冗余一份主键值,索引体积也跟着变大。
**为什么不要 SELECT ***
SELECT * 会返回所有列,一是网络传输开销大,二是如果表里有 TEXT/BLOB 大字段,查询会把这些行都搬到内存,对内存和 IO 都是浪费;三是如果未来加了字段,业务代码里靠索引位置取值的逻辑会直接崩。正确做法是只查需要的字段。
int(5) 和 int(10) 有区别吗
这是热词里 mysql int+5 对应的知识点。在 MySQL 8.0 之前,括号里的数字是显示宽度,配合 ZEROFILL 用,比如 INT(5) 插入 1 会显示为 00001,但它完全不限制存储的数值范围。INT 的存储范围只由 INT 这个类型决定,最大 21 亿,和括号里写 5 还是 11 没有关系。MySQL 8.0 已经把显示宽度废弃了,建表时写 INT(11) 也不会报错,但已经没有实际意义。面试官问这个就是看你对基础概念的理解是否深入。
REPLACE INTO 和 INSERT ... ON DUPLICATE KEY UPDATE 的区别
REPLACE INTO 遇到主键或唯一键冲突时,会先把冲突行 DELETE 掉,再 INSERT 新行。这会导致两个问题:一是自增 ID 会变,二是如果有外键级联删除,可能把关联表数据带没。ON DUPLICATE KEY UPDATE 是直接 UPDATE 这条记录,不会删除原行,更安全。所以业务里优先用 ON DUPLICATE KEY UPDATE,REPLACE INTO 尽量不用,除非你明确知道后果。
最后说一个自己的实操习惯。每次建表或改表前,我都会把 SQL 先在测试环境跑一遍,用 EXPLAIN 看一下执行计划,再切到生产。不要迷信网上那些模板,生产环境里的表动辄几百万行,改一个字段类型可能直接拖垮主库。表的基本操作看着基础,但它是所有数据安全的起点,宁可多花五分钟想清楚,也不要事后花五个小时恢复数据。
