很多人刚接触 MySQL 时,一看到“增删改查”四个字,条件反射想到INSERT、SELECT、UPDATE、DELETE这四条 DML 语句。但“表”本身的增删改查——建表、看表、改表结构、删表——才是真正让新手和老手拉开差距的地方。我见过不少开发写了几个月 SQL,CREATE TABLE照样写得随意,等字段类型选错、默认值没配好、ALTER 一张大表把线上服务锁死,才知道当初建表那几分钟偷的懒,要在后面用无数个加班来还。
这篇是 MySQL 系列的第 5 期,专讲表自身生命周期的四个动作:创建(Create)、查看(Read)、修改(Update)、删除(Delete)。适合刚学完基础语法、准备正经设计业务表的同学,也适合那些已经写了两三年 SQL 但没系统梳理过 DDL 细节的开发。读完你至少能避开我踩过的那些坑,知道为什么有些表敢随便 ALTER,有些表动一下就得提前写申请。
1. 建表(Create):设计期的每一步选择,都在给未来填坑
1.1 建表语法全貌:一行行拆开看更安心
CREATE TABLE的完整语法,很多人从来没用全过。但我的经验是:宁可花十分钟把每个可选项都过一遍,也不要等到表建完、数据进去了,再回头折腾结构。
CREATE TABLE [IF NOT EXISTS] `表名` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `user_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名', `mobile` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1正常 0禁用', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_mobile` (`mobile`), KEY `idx_user_name` (`user_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表';先别急着复制。注意几个容易被忽略的细节:IF NOT EXISTS不是摆设,脚本重跑时能救命;字段名用反引号包裹,是为了防关键字冲突;每个字段都写COMMENT,时间一长你就明白注释有多重要——你三个月后看一个没注释的status字段,绝对会骂自己当初为什么不写清楚。
1.2 字段类型怎么选:选错了后面全是重建
这是建表时最核心的部分。选类型不能只看“能不能存下”,还要看存储空间、索引效率、运算方式和扩展性。选错类型表建完了,后面改起来就是动一发牵全身。
整数型:
TINYINT(1字节)、SMALLINT(2字节)、MEDIUMINT(3字节)、INT(4字节)、BIGINT(8字节)。选型的唯一标准是范围,不要什么都用BIGINT。举个例子,状态字段 0~255 就能覆盖,TINYINT足够;主键如果可能超过 42 亿,直接用BIGINT UNSIGNED,别等到表里有几千万行再迁徙主键,那是灾难级别的操作。小数:金额等需要精确计算的场景,坚决用
DECIMAL,不要用FLOAT/DOUBLE。二进制浮点数的二进制存储特性导致精度丢失,最典型的例子:0.1 + 0.2 != 0.3,这在财务报表里是致命的。DECIMAL(10,2)表示最长 10 位、小数部分 2 位,能存 99999999.99,足够大多数业务用。字符串:长度短且固定的用
CHAR,比如身份证号、订单号;长度变化的用VARCHAR。但VARCHAR不是无限长,超过一定长度之后,行溢出会影响性能。TEXT类型也不要滥用——它不能有默认值,而且索引只能有前缀索引,能拆表的尽量拆出去。日期时间:
DATETIME和TIMESTAMP是最容易踩坑的一对。TIMESTAMP只有 4 字节,2038 年会溢出;DATETIME是 8 字节,范围 1000 到 9999 年,所以新表一律推荐DATETIME。另一个坑:TIMESTAMP会自动跟随时区,DATETIME不跟随,如果你的业务跨时区,这里面很容易闹出“同一时刻两边看到的创建时间不一样”的问题。
字段类型选型的核心原则,我总结成一句话:能用整数就不用字符串,能用定长就不用变长,能用短的就不要用长的,精确计算的永远用 DECIMAL。
1.3 约束设计:NOT NULL、DEFAULT 和自增列的隐藏影响
很多人建表时不写NOT NULL,觉得无所谓。但如果你在业务代码里写if (row.getXxx() != null)这样判断,NULL 的存在会让逻辑复杂不止一个级别。更关键的是,NULL和''在索引、COUNT、WHERE判断中的行为完全不同,索引对大量NULL值的处理效率也更差。
我个人的实践偏好:尽可能让所有字段 NOT NULL,用 DEFAULT 填充默认值。像用户昵称就设DEFAULT '',更新时间就设DEFAULT CURRENT_TIMESTAMP。这样既保证了写入的确定性,也简化了业务层的判断。唯一允许NULL的,通常是那些业务上确实无法确定的字段,比如用户补充填写的手机号、备注信息。
自增列AUTO_INCREMENT的坑更多。第一个坑:它是基于当前值+步长递增的,但一旦删掉最大值,MySQL 可能会复用这个值(在非 InnoDB 或有特定配置时),业务如果拿自增 ID 当订单号用,就会出现“订单号跳号”或“重复”的表象。实际应用中订单号这类有业务含义的编号,应该单独做发号器,不要依赖自增主键。第二个坑:自增列必须是索引,通常配合主键使用,如果你建表时忘记写PRIMARY KEY,那AUTO_INCREMENT会直接报错。还有一个隐蔽点:同一张表里只能有一个自增列,而且它必须被定义为NOT NULL,这几个条件缺一不可。
1.4 存储引擎和字符集:utf8mb4 不止是中文的问题
ENGINE=InnoDB在 MySQL 5.5 之后就是默认引擎,这条基本不用多想。但字符集问题值得单独拎出来说。老项目里经常能看到utf8,这个字符集在 MySQL 里是“残缺版”,它最多只能存 3 字节的 UTF-8 编码,遇到生僻字、emoji 表情(4 字节)就会报错或者存成?。
所以新建表一律CHARSET=utf8mb4。MySQL 8.0 的默认字符集已经是 utf8mb4 了,5.7 还得手动指定。COLLATE排序规则里,utf8mb4_0900_ai_ci是 MySQL 8.0 的默认,utf8mb4_general_ci兼容性更好。除非你明确知道要区分大小写排序,否则默认就够。字符集选错最痛苦的地方在于,它不太容易通过ALTER TABLE快速修正——表里的历史数据已经按旧字符集编码存进去了,强行转换会产生乱码。
建表最后一步,建议再用SHOW CREATE TABLE 表名;检查一遍实际生成的 SQL,和你的设计稿做对比。很多可视化工具会对 DDL 做“过度加工”,你以为建了一张简单的表,实际生成的 DDL 里可能带了奇怪的默认值或索引。
2. 查看表(Read):工具和 SQL 之间隔着一个视角
2.1 从 SHOW TABLES 说起
最基础的查看表的操作,就是列出当前数据库里所有的表:
SHOW TABLES;这个命令虽然简单,但加个LIKE条件就比SHOW TABLES好用得多,尤其库里面表数量有成百上千张的时候:
SHOW TABLES LIKE 'user%';不过我工作里更喜欢直接查information_schema.tables,因为它能同时返回每张表的存储引擎、行数、数据大小、创建时间等元数据,一张 SQL 把想看的全带出来:
SELECT TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH + INDEX_LENGTH AS total_size, TABLE_COMMENT FROM information_schema.tables WHERE TABLE_SCHEMA = 'your_db_name' ORDER BY total_size DESC;2.2 SHOW CREATE TABLE 才是表结构的准绳
DESC 表名;和SHOW CREATE TABLE 表名;是查看表结构的两大主力,但两者视角不同。
DESC用法直接,一眼扫过去能看到字段名、类型、是否为空、默认值、附加属性,适合日常快速浏览。但它有个缺点:不显示索引和表级配置。我经常遇到的情况是,开发同学用DESC看完一张表,以为主键没问题,结果实际表里根本没有主键,或者索引建错了——这些DESC不直接显示。
SHOW CREATE TABLE才是判断表结构真面目的唯一依据,它会把 MySQL 服务端最终执行的建表语句原样输出,包括所有索引、约束、引擎、字符集。我强烈建议任何一次数据库结构变更前,先跑一下这个命令,把当前 DDL 存着,变更后做一个 diff,就知道自己到底改了什么东西,也方便后续回滚。
2.3 information_schema:比 DESC 更接近真相
information_schema.column这个视图,是我查看表结构时绕不开的一张表。它把每一张表的每一个字段,作为一行元数据存起来,包括数据类型、字符集、注释、是否可空等。
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'your_db_name' AND TABLE_NAME = 'orders' ORDER BY ORDINAL_POSITION;这有什么用?举个实际场景:你要在代码里动态生成对账单、导出 Excel 的列头,与其手工维护字段名和注释的映射,不如直接跑这条 SQL,把COLUMN_COMMENT拉出来作为表头,准确度比人工维护高得多。
另一个从information_schema.tables里拿到的关键指标是TABLE_ROWS。必须提醒一下:对 InnoDB 表来说,这个数字只是一个采样估算值,不是精确值,别拿它去做统计报表。想知道精确行数,只能COUNT(*),否则你会被“表里明明一百万行但查出来只有八万行”这种假象坑死。反过来,它也有优势:估算行数不用扫全表,对超大表来说,稍微牺牲一点精度换查询速度,是值得的。
3. 修改表(Update):ALTER TABLE 虽常用,却最逼近风险线
3.1 加字段 / 删字段:顺序和数据量是重点
开发流程里最频繁的操作就是加字段。语法本身很简单:
ALTER TABLE `orders` ADD COLUMN `coupon_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '优惠券ID' AFTER `amount`;有三个容易被忽略的坑。第一个是AFTER和FIRST的位置指定:如果加到大表的末尾,MySQL 8.0 可能走 INSTANT 算法,秒级完成;但如果你指定AFTER 某字段,就可能触发表重建,大数据量下直接锁表。所以线上给大表加字段,能加到末尾就加到末尾,位置美观在这种场景下不重要。第二个是多个字段要一起加,不要一条条 ALTER,一条 SQL 里可以同时加多个字段:
ALTER TABLE `orders` ADD COLUMN `coupon_id` BIGINT UNSIGNED NOT NULL DEFAULT 0, ADD COLUMN `discount_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00;每一条 ALTER 都要重新解析表结构、拿元数据锁,拆成多条只会增加操作时间和风险窗口。第三个坑:删除字段时,如果这个字段上有索引,索引会一并被删除;如果有外键关联,删除会直接失败。删字段前最好先用SHOW CREATE TABLE看清楚依赖关系。
3.2 修改类型和重命名:别让你的 SQL 变成炸弹
MODIFY COLUMN和CHANGE COLUMN是改字段的两条路,区别在于CHANGE可以顺便把字段名一起改了,MODIFY不能改字段名。
ALTER TABLE `users` MODIFY COLUMN `mobile` VARCHAR(30) NOT NULL DEFAULT '' COMMENT '手机号'; ALTER TABLE `users` CHANGE COLUMN `mobile` `phone` VARCHAR(30) NOT NULL DEFAULT '' COMMENT '手机号';这里的核心风险是:修改字段类型通常会导致全表重建。比如VARCHAR(50)改成VARCHAR(100),如果长度增长但行内放得下,5.7+ 的 InnoDB 可以走 INPLACE 算法,不一定全表 copy。但把VARCHAR改成TEXT、把INT改成BIGINT,十有八九要重建表,并对数据做隐式转换。那种几千万行的表,一次这样的操作可能是几十分钟到几个小时的 DDL,期间对表的写入会被阻塞。
更危险的还属“改字段名”。很多 ORM 框架的映射关系是跟字段名绑定的,线上业务代码改字段名,等于要求代码同时上线发布,任何一个环节不一致,轻则查询报错,重则丢掉数据。我见过一次同学改完字段名,忘记把增量同步任务里的 SQL 同步改掉,结果同步任务跑了半小时,目标表里写入了一批带NULL的记录,花了两个下午才清洗干净。给已有字段重命名,我是不到万不得已不动手的。
3.3 ALTER 的锁与 instant:版本决定命运
ALTER TABLE 之所以危险,主要取决于它内部使用的算法:COPY、INPLACE还是INSTANT。很多人不知道 MySQL 的 ALTER 分为这三种底层策略,我来解释一下。
- COPY:最古老最粗暴的方式。MySQL 会创建一个临时表,把原表数据一条条拷进去,完成后删掉原表。这个过程中原表通常不可写,只能读,几千行的小表无所谓,几千万行的表就是线上事故的源头。
- INPLACE:从 MySQL 5.6 开始支持,不复制数据,基于原始表文件原地修改,兼顾读写。但 INPLACE 内部仍可能做重建,只是底层的 IO 策略比 COPY 高效得多,而且支持
LOCK=NONE,允许 DML 并发。 - INSTANT:MySQL 8.0.12 引入,只修改数据字典,不触碰数据文件。目前主要支持“在表末尾新增字段”等少数操作。
所以我个人的 ALTER 操作顺序是:先用SHOW CREATE TABLE确认表结构和引擎,然后用information_schema.tables看行数和数据大小评估风险,最后加上ALGORITHM和LOCK参数显式控制行为:
ALTER TABLE `orders` ADD COLUMN `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注', ALGORITHM=INSTANT, LOCK=NONE;如果当前 MySQL 版本不支持 INSTANT,它会报错而不是偷偷降级成 COPY,这一点很关键——它逼着你确认操作可执行,而不是让你线上偷偷跑一场全表重建。
这里必须再提一个大表 ALTER 的通用经验:永远不要在业务高峰期对超过百万行的表做结构变更。哪怕它理论上是 INPLACE、LOCK=NONE,高并发写入下 DDL 依然会产生大量临时文件 IO,导致主从延迟飙升。生产环境要做,建议大家用专业工具(比如 Percona Toolkit 里的在线改表方案)分块处理,或者选择凌晨低峰期操作,并且提前做好回滚预案。
4. 删除表(Delete):DROP、TRUNCATE 与 DELETE 的取舍
4.1 行列级对比:三条语句不是一回事
表相关操作里,最容易被搞混的就是DROP、TRUNCATE和DELETE。虽然都是“删”,但三者做的事完全不同:
| 维度 | DELETE | TRUNCATE | DROP |
|---|---|---|---|
| 对象 | 表中的数据行 | 表中的数据行 | 整张表(结构+数据+索引) |
| 是否可回滚 | 可以,配合事务回滚 | 不可,InnoDB 下也无法逐行回滚 | 不可 |
| 速度 | 逐行删除,慢,有 undo log | 直接释放数据页,极快 | 极快,但依赖表文件删除 |
| 自增计数 | 不动 | 重置 | 表没了 |
| 空间 | 数据页标记复用,空间不立即释放 | 释放空间(可收缩表文件) | 释放空间 |
| 条件 | 支持 WHERE | 不支持条件,全表清空 | 无 |
日常业务清理数据,带WHERE条件的删除必须用DELETE并确保走索引;确认要清空表数据但保留表结构,用TRUNCATE最合适,比如清空临时表、重置测试库;整个表都不要了,直接用DROP TABLE,带着IF EXISTS防止脚本重跑报错:
DROP TABLE IF EXISTS `temp_log`;注意TRUNCATE在 InnoDB 里会重置自增主键的计数值,如果你清空一张表后又希望从 1 开始分配 ID,这反而是特性。但如果你只是要删掉部分脏数据,千万不要用 TRUNCATE,这是无法局部回滚的。
4.2 误删之后:没有后悔药的数据库
在所有表操作里,我来来回回说得最多的就是警惕误删。DELETE 还能靠事务回滚抢救,TRUNCATE 和 DROP 一百万个后悔都换不回来。一个真实的工作场景:凌晨三点发现线上数据被某条清理任务误删,目标表本该TRUNCATE temp_a,由于写 SQL 时复制粘贴错误少了个temp_前缀,直接清了正式表,近三百天的订单明细全部灰飞烟灭。最后靠前一天的物理备份加 binlog 增量恢复了六个多小时,业务整整停更了一个上午。
所以有几个习惯我强烈建议每个数据岗、开发岗的人刻进肌肉记忆:
- 删除前备份:无论 DELETE 多少行,先
CREATE TABLE xxx_bak_20250101 AS SELECT * FROM xxx WHERE ...,把需要删除的数据备份一份,成本低收益高。 - 先 SELECT 后 DELETE:凡是带条件的 DELETE,先把 WHERE 条件换成
SELECT COUNT(*)跑一遍,确认行数符合预期再执行删除。条件一旦写错,计数的结果不会骗你。 - 使用安全模式:MySQL 客户端或可视化工具开启 safe update 模式,不带 WHERE 的 UPDATE/DELETE 直接拒绝执行,杜绝把“整表操作”手滑成“全表拉闸”。
- 权限分层:生产库尽量不给普通开发账号 TRUNCATE/DROP 权限,DDL 操作走工单流程,两个人确认后执行,比你一个人戴着耳机凌晨操作安全得多。
4.3 外键与表空间:删除表前你要知道的影响
删除一张表不只是把它从库里抹掉这么简单。先看外键关系:如果别的表通过外键引用了这张表,DROP 会直接失败,报错信息会提示有外键约束。生产环境里很多团队干脆禁止使用物理外键,改为应用层维护数据一致性,但这也是一个需要权衡的决定——外键带来的约束和保护是实打实的。
还有磁盘空间的错觉。很多人删完表跑df -h,发现可用空间没怎么涨,这不奇怪。删除表后,InnoDB 表空间的大小取决于配置。如果innodb_file_per_table=ON(MySQL 5.6 后默认开启),每张表独立表空间,DROP 后操作系统层面能看到空间释放;如果全局共享表空间,DROP 只是把空间标记为空闲,只有再次向表空间写入数据时空间才会被复用。所以排查“删了表怎么磁盘没释放”之前,先看下表的存储模式和配置。
表空间收缩问题也值得提一嘴。删除大量行之后,表文件大小不变,因为数据页只是被标记为空闲。想让物理文件缩回来,就得重建表或者OPTIMIZE TABLE。一张有大量数据碎片的大表,OPTIMIZE TABLE也可能锁表或耗时很长,需要放在低峰期,并先评估磁盘是否有足够的临时空间来完成重建。
个人经验的一点补充:删除操作尽量和外键一起检查,用一条 SQL 查出当前库所有指向目标表的引用关系:
SELECT TABLE_NAME, CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = 'orders' AND TABLE_SCHEMA = 'your_db_name';看到结果再决定怎么处理,不要硬删,硬删的代价可能是删完发现整个业务流程中间断了一环,后续数据对账对不上,那才是真正的大麻烦。
最后分享一个我自己的体感:这期讲的四类操作,真正难的不是语法,而是对“代价”的感知。刚入行的时候我也爱背语法,一行 CREATE TABLE 敲得飞快,后来在线上被几场 ALTER 和大表清理教育过之后,才明白建表时的每个约束、每次查询前的检查、每次删除前的备份,都是在给未来的自己买保险。弄懂这些原理,不是让你操作得更快,而是让你在操作前多想一次:这一条 DDL 下去,数据库要付出多少 IO 和锁代价,业务能不能等得起。带着这种判断,你写出来的 SQL 会稳很多。