1. 为什么你需要一份DDL速查手册
先说个场景。前几天在群里帮一个刚转后端的朋友排查问题,他要把线上订单表的一个字段从int改成bigint,结果直接把整个表给锁了。业务高峰期,订单表几千万行数据,一条ALTER TABLE下去,写入直接卡死,过了十几分钟才缓过来。他当时的原话是:“我就改了个字段类型,怎么还把表锁了呢?”
这就是典型的DDL基本功不扎实。DDL(Data Definition Language)是数据库的“地基语法”,它控制着库、表、索引、约束这些元数据的增删改查。相比DML(Data Manipulation Language)天天操作具体数据,DDL虽然用得没那么频繁,但每次使用都是“大动干戈”,一旦出错,影响的是整个表甚至整个库。
这也是我写这篇文章的初衷。日常开发中,SELECT、INSERT、UPDATE这类语法大家张口就来,但到了建表、改表、加索引、加约束这些DDL操作的时候,很多人的反应是“查一下文档”“翻一下旧代码”。不是说查文档不好,而是DDL里的很多细节,比如MODIFY和CHANGE的区别、VARCHAR和CHAR在索引上的不同表现、ALGORITHM=INPLACE和COPY的区别,光靠临时查资料很难建立系统认知。等你真正踩坑了,往往已经晚了。
这篇文章不讲花哨的东西,就是一份基于MySQL 8.0(兼容5.7)的DDL实操总结。从库管理到表管理,从索引到约束,再到我这些年踩过的坑和排查过的故障,一次性梳理清楚。不管你是我那位刚转后端的朋友,还是工作了三五年想夯实基本功的开发者,这份手册应该都能帮你省下不少时间。
2. DDL全景图:四大类操作先分清
在开始敲命令之前,脑子里先有一个宏观的框架。DDL按操作对象大致可以分为四个层级:库(Database)、表(Table)、索引(Index)、约束(Constraint)。
这四个层级之间的关系可以理解为一个大楼的结构:数据库是整栋楼的外壳,表是楼里的各个房间,索引是房间门口挂的指引牌,约束则是水电管线的阀门开关。拆除顺序跟搭建顺序刚好相反,先拆开关,再拆指引牌,再拆房间,最后才拆外壳。
从语法角度来说,每个层级都对应着一组核心动词:CREATE(创建)、ALTER(修改)、DROP(删除)、RENAME(重命名)、TRUNCATE(清空)。把这些动词和层级组合起来,就构成了DDL的大部分操作面。
我建议你在学习时建立一个这样的对照表:
| 操作对象 | 创建 | 修改 | 删除 | 其他 |
|---|---|---|---|---|
| 数据库 | CREATE DATABASE | ALTER DATABASE | DROP DATABASE | SHOW DATABASES |
| 表 | CREATE TABLE | ALTER TABLE | DROP TABLE / TRUNCATE | RENAME TABLE |
| 索引 | CREATE INDEX | 无(通过ALTER TABLE间接改) | DROP INDEX | SHOW INDEX |
| 约束 | CREATE TABLE时定义 | ALTER TABLE ADD/DROP | 同左 | 无 |
这个框架看起来简单,但很多人在实际操作中会把语法混着用。比如删除索引,有人会记成DROP INDEX ON table,还有人会记成ALTER TABLE ... DROP KEY,其实这两种写法在MySQL里都行,但如果你不清楚它们都指向同一个操作,遇到语法报错就会懵。
下面具体展开每一层级的语法和实操细节。
3. 数据库级DDL:不只是建个库那么简单
数据库层面的DDL是最容易被忽略的,因为大多数项目在初始化时建一次库就完事了。但恰恰是这个“只操作一次”的操作,如果字符集、排序规则选错了,后面改起来非常痛苦。
3.1 CREATE DATABASE:字符集和排序规则的选择
建库的语法很简单:
CREATE DATABASE [IF NOT EXISTS] db_name [DEFAULT] CHARACTER SET charset_name [DEFAULT] COLLATE collation_name;重点在于CHARACTER SET和COLLATE的选择。MySQL 8.0的默认字符集是utf8mb4,这个不用纠结,直接沿用就好。utf8mb4是真正的“全量Unicode”,支持四字节的字符,包括emoji、生僻字等。而经典的utf8(在MySQL里叫utf8mb3)只支持三字节,存不了四字节字符,遇到emoji就直接报错。
这里有个非常典型的坑:字符集utf8mb4下,VARCHAR(255)的“255”是字符数上限,不是字节数。一个中文字符在utf8mb4下占3到4个字节,如果是四字节emoji,255个字符最多能占1020字节。在InnoDB的COMPACT行格式下,单列变长字段的长度表示是有上限的,超出限制会报Row size too large错误。所以别仗着VARCHAR(255)够用就随意挥霍,该控制长度还是要控制。
排序规则(COLLATE)也要在库级别定好。MySQL 8.0默认是utf8mb4_0900_ai_ci(不区分大小写),5.7默认是utf8mb4_general_ci。如果你的项目对大小写敏感,比如用户名登录要做精确匹配,可以在建库时指定utf8mb4_bin或utf8mb4_0900_as_cs。
实操示例:
CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci;3.2 ALTER DATABASE与DROP DATABASE:会改更要会删
ALTER DATABASE用得不多,主要场景是修正此前建库时的字符集配置。语法是:
ALTER DATABASE db_name CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;注意,这个操作只会修改数据库db_name的默认字符集,对已存在的表、已存在的字段不会生效。很多新手以为改完库的字符集,表里的乱码就自动解决了,这是误解。实际上你要逐表、逐字段地去改字符集,或者在建库时就确认好配置,避免后续返工。
DROP DATABASE是真正的“一键毁灭”,连表带数据全没了。生产环境操作前必须确认备份、确认环境,不要因为多打了一个环境名就把线上库删了。我见过因为DROP DATABASE test_db写错环境名导致线上库被删的事故,那一夜的星光格外刺眼。所以生产环境务必遵循最小权限原则:开发账号不给DROP权限,DBA自己操作时也要用跳板机走审批流程。
3.3 查看库信息:SHOW DATABASES与SHOW CREATE DATABASE
SHOW DATABASES能列出当前实例的所有库,SHOW CREATE DATABASE db_name能查看某个库的建库语句,这是校验字符集配置最快的方式:
mysql> SHOW CREATE DATABASE shop\G *************************** 1. row *************************** Database: shop Create Database: CREATE DATABASE `shop` /*!40100 DEFAULT CHARACTER SET utf8mb4 */ /*!80016 DEFAULT ENCRYPTION='N' */注意那个/*!40100 ... */,这是MySQL的版本化注释语法,只在对应特性版本上才生效。看到这个别慌,说明这条语句在MySQL 4.01及以上版本执行时会带上注释中的内容。
4. 表级DDL:CREATE TABLE的完整打开方式
表是DDL的核心战场。很多人建表喜欢用可视化工具直接点生成,这没问题,但如果连生成的SQL里每一段是什么意思都看不懂,那就很危险了。
4.1 基础建表语法拆解
一条完整的建表语句大概长这样:
CREATE TABLE `user` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', `age` TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄', `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_username` (`username`), KEY `idx_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';逐段看几个核心要点:
BIGINT UNSIGNED:主键用BIGINT而不是INT,多不了几个字节,但能让你在数据量稍微大点时不用回头改表。UNSIGNED让可存储的数值范围翻倍,对主键来说是个白捡的好处。AUTO_INCREMENT:自增主键。注意在MySQL 8.0里,AUTO_INCREMENT必须是索引列(通常是主键),并且一张表只能有一个自增列。DATETIME和TIMESTAMP:我建表默认用DATETIME。TIMESTAMP在2038年会溢出(著名的2038年问题),而且受时区影响,DATETIME没有这些问题。ON UPDATE CURRENT_TIMESTAMP:更新数据时自动刷新时间,很实用,是审计字段的标配。- 注释(
COMMENT):字段注释和表注释一定要写。时间久了没人记得这个字段是干嘛的,注释是留给未来的自己和其他同事的“光”。
4.2 字段类型选择:用错了真的会出事
字段类型是建表时最影响后续性能的选择,我用常见业务场景给你拆解一遍:
- 整数类型:
TINYINT(1字节)、SMALLINT(2字节)、MEDIUMINT(3字节)、INT(4字节)、BIGINT(8字节)。按需选择,比如年龄用TINYINT UNSIGNED就够(0~255),订单金额如果用整数分存,BIGINT更稳妥。 - 浮点类型:
FLOAT和DOUBLE是浮点数,会有精度损失,涉及金额计算千万别用。要么用DECIMAL,要么用“整数分”存储并另加一个精度字段。 - 字符串类型:
CHAR定长,适合长度固定的数据(手机号、身份证号);VARCHAR变长,适合长度不固定的数据(用户名、邮箱)。注意VARCHAR不仅要存数据,还要额外用1到2字节存长度信息。 - 文本类型:
TEXT、MEDIUMTEXT、LONGTEXT用来存长文本。但如果你给TEXT列建索引,只能指定前缀长度,而且排序时会有临时表使用磁盘的风险。能用VARCHAR就不要轻易上TEXT。 - 时间类型:
DATE存日期,DATETIME存日期时间,TIMESTAMP带时区,TIME存时间。落实到业务,创建时间、更新时间统一用DATETIME基本没错。
4.3 存储引擎和字符集写在表级别
建表语句末尾的ENGINE=InnoDB DEFAULT CHARSET=utf8mb4不是可选项,而是明确你要什么。MySQL 8.0里InnoDB是默认引擎,但如果你从5.7迁移过,或者接手了老项目,一定要确认你的核心业务表都是InnoDB。MyISAM的全文索引和压缩在某些场景有优势,但它不支持事务、不支持行级锁、崩溃恢复能力差,核心业务表用了就是埋雷。
表级别的CHARSET和COLLATE不写,会继承库的默认值。库的默认值不写,会继承MySQL实例的配置。这种“继承链”看起来很省事,但容易造成一张库里的表字符集不统一。Join两个字符集不同的表,如果字段本身的排序规则不一致,轻则隐式转换导致索引失效,重则直接报Illegal mix of collations错误。所以建表时显式写好,别省这几个字。
4.4 临时表和复制表:两个实用的取巧方式
CREATE TEMPORARY TABLE用于创建只在当前会话可见的临时表,连接断开后自动消失。这在复杂统计、批量更新中间结果时非常好用:
CREATE TEMPORARY TABLE tmp_user_snapshot AS SELECT id, username, created_at FROM user WHERE created_at >= '2024-01-01';CREATE TABLE ... LIKE是复制表结构最快的方式,比如做表结构变更前的演练:
CREATE TABLE user_bak LIKE user;注意LIKE只复制表结构、默认值和约束,不复制数据。要复制数据的话再加一句INSERT INTO user_bak SELECT * FROM user;。
5. ALTER TABLE:改表结构的完整姿势
ALTER TABLE是DDL里最容易出问题的部分。生产环境大表执行ALTER TABLE,如果没人盯着,很可能把核心业务锁死。这一章把它的每一种用法都拆开讲一遍,再讲怎么安全地在生产环境操作。
5.1 ADD COLUMN:加字段的正确姿势
ALTER TABLE user ADD COLUMN `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称' AFTER `username`;AFTER关键字控制新字段在表中的物理顺序,默认是加在最后面。从性能和使用角度讲,字段顺序影响不大,但在SELECT *查询(这种写法本来就不推荐)里,字段顺序会影响结果集的展示顺序,有点强迫症的人会喜欢调整一下。
加字段时DEFAULT值要仔细确认。老数据会自动填充默认值,如果你给了一个不允许为空的字段但没有默认值,ALTER TABLE会先在表里塞满新字段值。如果表特别大,这个过程会锁表较长时间。所以生产环境加字段,优先允许为空,或者给一个确定的默认值,然后分两步走:先加可空字段,再通过业务代码补数据。
5.2 MODIFY COLUMN:只改类型和默认值
MODIFY用于修改字段的“属性”,不改字段名。比如调整nickname的长度:
ALTER TABLE user MODIFY COLUMN `nickname` VARCHAR(100) DEFAULT NULL COMMENT '昵称';注意MODIFY命令中必须包含完整的新类型定义,不然你没写的属性会被重置。我见过有人执行ALTER TABLE t MODIFY COLUMN c1 VARCHAR(20);,结果字段原有的NOT NULL约束被干掉了,因为语法里没有重新声明它。这里有一个默认值“规则”,MODIFY实际上对字段定义做了一次整体替换。
5.3 CHANGE COLUMN:同时改名字和定义
CHANGE是MODIFY的加强版,可以改名字、改类型、改默认值:
ALTER TABLE user CHANGE COLUMN `nickname` `nick_name` VARCHAR(80) DEFAULT NULL COMMENT '昵称';这个命令的语义是“把旧列改成新列,新列定义如下”。使用CHANGE时同样要把所有属性写全,否则就是一次隐性的“重置”。实际工作中,除非确实需要改字段名,否则我更推荐用MODIFY,少一个变更维度就少一分风险。
注意:
MODIFY和CHANGE都会触发表的重建(取决于算法)。在5.7及更早版本中,很多类型修改需要用ALGORITHM=COPY,意味着完整拷贝表数据。8.0中InnoDB支持了更多的ALGORITHM=INPLACE场景,但并非所有操作都能免拷贝。
5.4 DROP COLUMN:删字段前先检查依赖
删除字段的语法很简单:
ALTER TABLE user DROP COLUMN `nickname`;但删字段前务必确认三件事:有没有索引引用该字段(索引会连带失效)、有没有视图或存储过程引用该字段、有没有业务代码在SELECT *里依赖这个字段。第二点尤其容易被忽略,视图不会在建表时校验引用关系,删了字段后视图可能就悄悄坏了,等你跑某个报表时才发现数据不对。
另外,大表DROP COLUMN一样会在执行期间对表加元数据锁,影响写入。如果是超大表,建议参考后文“Online DDL”部分,规划窗口期操作。
5.5 RENAME TABLE:一条语句搞定多表重命名
RENAME TABLE的语法很简单:
RENAME TABLE user TO user_old, user_new TO user;我经常用这个特性做“换表”操作,比如在不停服的情况下把数据从备表切入正式表:先把新表切到暂用名,把旧表改名存档,再把新表改回正式名。这个操作是原子的,配合业务侧短暂的只读开关,基本可以做到无缝切换。
6. 索引级DDL:从创建到删除的完整闭环
索引是MySQL性能优化的第一抓手,但很多人对索引DDL的了解只停留在CREATE INDEX idx ON table (col)这个层面。其实索引的创建方式、失效场景、选择原则,每一项都有讲究。
6.1 索引分类与CREATE INDEX语法
MySQL的索引可以从不同维度分类:
- 普通索引(INDEX/KEY):最基本的索引,没有任何限制,目的就是加速查询。
- 唯一索引(UNIQUE KEY):索引列的值必须唯一,允许有一个NULL(InnoDB下唯一索引的NULL是互斥的,多个NULL会冲突,所以实际创建会失败或报错,这个要分版本核对)。
- 主键索引(PRIMARY KEY):一种特殊的唯一索引,不允许为NULL,一张表最多一个。
- 全文索引(FULLTEXT):用于全文检索,MyISAM和InnoDB都支持。中文场景下,MySQL自带的分词效果一般,真要全文检索还是用专门的搜索引擎。
- 组合索引:多个字段组成一个索引,遵循最左前缀原则。
- 前缀索引:对
VARCHAR/TEXT列的前N个字符做索引,能省空间,但有额外计算成本。
新建索引有两种等价写法:
-- 写法一:ALTER TABLE 语句追加索引定义 ALTER TABLE user ADD UNIQUE KEY `uk_username` (`username`); -- 写法二:独立的CREATE INDEX语句 CREATE UNIQUE INDEX `uk_username` ON user (`username`);两种写法最终效果一样,我习惯用ALTER TABLE统一管理表上的索引,因为能看到完整的上下文。
6.2 创建组合索引的字段顺序选择
组合索引的设计是索引DDL的核心难点。原则一句话:区分度高的字段放前面,等值查询的字段优先,范围查询的字段尽量放后面。
举个例子,业务上高频查询是WHERE status = 1 AND created_at >= '2024-01-01',那么索引顺序应该是(status, created_at)而不是(created_at, status)。因为status是等值匹配,created_at是范围查询。如果把created_at放前面,status的等值条件只能作为range之后的过滤,索引利用会打折扣。
再补一个容易被坑的点:不要在索引列上做运算。WHERE DATE(created_at) = CURDATE()这样写,索引会完全失效。正确的做法是改成范围查询:WHERE created_at >= '2024-01-01 00:00:00' AND created_at < '2024-01-02 00:00:00'。这个优化不涉及DDL,但如果你在建索引后才发现查询没走索引,八成是这类写法问题。
6.3 前缀索引:省空间的代价
假设你要对一列VARCHAR(255)的url建索引,整个字段索引不仅占空间,而且因为字段太长,B+树的非叶子节点存储能力下降。这时候可以只对前N个字符建索引:
ALTER TABLE page ADD KEY `idx_url` (`url`(64));N怎么定?目标是在保证区分度的前提下选最短长度。可以用一个技巧来评估:
SELECT COUNT(DISTINCT LEFT(url, 32)) AS prefix_count_32, COUNT(DISTINCT LEFT(url, 64)) AS prefix_count_64, COUNT(DISTINCT url) AS total_count FROM page;如果prefix_count_64和total_count很接近,基本可以确定64的前缀长度够用。代价是前缀索引在排序和覆盖索引上有限制,ORDER BY url无法使用前缀索引做排序。
6.4 DROP INDEX:删除索引的正确时机
删除索引的语法很简单:
DROP INDEX idx_name ON table_name; -- 等价于 ALTER TABLE table_name DROP KEY idx_name;但“什么时候该删索引”比“怎么删”更重要。我见过很多增长速度很快的库,索引数量比字段还多,写入性能被严重拖累。判断索引该删时,可以这样检查:
- 用
SHOW INDEX FROM table_name查看表的索引清单。 - 打开慢查询日志和
performance_schema,找出从没被使用过的索引。 - 确认该索引没有被查询计划用到后,在业务低峰期删除。
注意:唯一索引即使查询没走到,它依然在保证数据唯一性,这种索引不能随意删,除非你能确定业务上不需要唯一性约束。
7. 约束级DDL:NOT NULL不是小事
约束是保证数据质量的第一道防线,DDL中的约束类型主要包括:PRIMARY KEY、FOREIGN KEY、UNIQUE、NOT NULL、DEFAULT、AUTO_INCREMENT、CHECK。
7.1 五种约束的作用与定义方式
主键约束(PRIMARY KEY):唯一标识一行,自动创建主键索引。InnoDB的聚簇索引就是主键,主键选择直接影响整个表的数据物理排列。推荐自增BIGINT主键,业务字段(身份证号、邮箱)不适合做主键,因为业务字段会变,且长度偏大。
唯一约束(UNIQUE KEY):保证列值的唯一性。数据同步、防重提交通常用唯一约束兜底。注意唯一约束允许NULL,但在MySQL 8.0中,多个NULL值会被视为不同,于是唯一约束实际上不限制NULL的个数。
外键约束(FOREIGN KEY):保证子表引用完整性。但外键在MySQL里的坑比较多:强制JOIN限制、数据导入麻烦、分库分表直接失效。互联网高并发场景基本不用外键,靠应用层保证数据关系。如果你在传统行业做管理系统,用外键也无可厚非,涉及强一致性的内部系统,外键的真香定律还是存在的。
非空约束与默认值(NOT NULL / DEFAULT):能用NOT NULL就用NOT NULL。尤其核心业务字段,一旦允许NULL,查询里到处都是IS NULL和IFNULL,还容易在计算时产生意料之外的NULL传播。
CHECK约束:MySQL 8.0.16开始真正支持CHECK约束。例如年龄范围:
CREATE TABLE user_profile ( id BIGINT PRIMARY KEY, age TINYINT, CONSTRint chk_age CHECK (age >= 0 AND age <= 150) );7.2 通过ALTER TABLE追加和删除约束
给已有表追加约束:
-- 追加唯一约束 ALTER TABLE user ADD UNIQUE KEY `uk_email` (`email`); -- 追加CHECK约束 ALTER TABLE user_profile ADD CONSTRAINT chk_age CHECK (age >= 0 AND age <= 150); -- 删除约束 ALTER TABLE user_profile DROP CHECK chk_age;追加NOT NULL约束要特别小心,因为老数据里可能有NULL,直接改会失败。正确姿势是先UPDATE补齐,再MODIFY加约束,必要时分批次更新避免锁表时间过长。
7.3 外键的“真香”与“真坑”
外键在MySQL里是双刃剑。如果你决定在项目里用外键,我建议你承认一个现实:你在牺牲可扩展性和部分性能,换取数据库层面的强一致性。用得好是保障,用不好是灾难。
实操中,外键使用最大的坑是:删父表数据时,如果子表有外键引用,删除会被拒绝。有人想绕过,先删子表再删父表,结果子表几百万行,一条DELETE下去直接锁范围扩大。这种场景我建议优先考虑:要么不用外键靠应用层解决,要么把级联删除策略设计好,看清ON DELETE CASCADE和ON DELETE SET NULL的区别再执行。
8. 生产环境实操:Online DDL和低峰窗口
这一章是我最想说的部分。很多DDL在测试库上执行几秒就完事了,但在生产环境,一条ALTER TABLE可能把你的核心业务干趴下,这不是危言耸听。
8.1 Online DDL:InnoDB的算法选择
MySQL 5.6以后引入了Online DDL,InnoDB支持在执行期间并发执行DML(某些操作),这个机制跟MySQL 8.0里的ALGORITHM和LOCK参数有关。
常见的算法选项:
ALGORITHM=INPLACE:在原表上进行修改,不复制整表数据,多为Online DDL,但具体是否完全Online还要看操作类型和LOCK设置。ALGORITHM=COPY:拷贝整表数据到新表并重建,过程中需要锁表(禁止写入),性能开销大,是早前版本的默认模式。LOCK=NONE:允许并发读写。LOCK=SHARED:允许并发读,不允许写。LOCK=EXCLUSIVE:读写都不允许,最严格的锁。
举个例子,给表加索引的语法可以显式声明算法:
ALTER TABLE user ADD KEY `idx_created_at` (`created_at`), ALGORITHM=INPLACE, LOCK=NONE;但要注意:并非所有操作都支持INPLACE。比如修改主键类型、把VARCHAR改成TEXT、在5.7及以下版本中修改字符集,都可能退化为COPY。8.0中支持的范围更广,但依旧有例外。
8.2 大表操作前的“四问”
每次要在生产环境执行DDL操作时,我都会先“四问”:
- 这个操作能在窗口期做吗?能,就排窗口期;不能,就评估Online DDL是否支持。
- 表有多大?100万行和1亿行的处理策略完全不同。
- 存在关联对象吗?视图、存储过程、外键、触发器,这些都会影响DDL的执行和兼容性。
- 有备份吗?让一个DBA养成习惯,操作前先备份,备份的时间和空间成本要提前评估。
8.3 借助工具:pt-osc与gh-ost
如果你的表真的很大(上亿行),而且InnoDB的Online DDL不是完全不锁写,那么开源工具会更靠谱。用得比较多的是Percona Toolkit里的pt-online-schema-change(简称pt-osc)和GitHub开源的gh-ost。
pt-osc的基本逻辑是:创建一张新表,按要变更的定义建好结构,然后从原表分批拷贝数据到新表,同时在原表上创建触发器把变更期间的增量数据同步过去,完成后原子地切换表名。关键参数:
pt-online-schema-change --alter "ADD COLUMN remark VARCHAR(255) DEFAULT NULL" \ D=shop,t=user --host=xxx --user=xxx --password=xxx --executegh-ost比pt-osc更轻量,不需要触发器。它通过解析binlog完成增量同步,对主库的压力更小。但gh-ost要求binlog格式为ROW,且有从库可连接(或者拓扑结构支持)。
这类工具引入的复杂性也不低,如果是中小型服务,升级到8.0、合理设计索引和字段,使用InnoDB Online DDL往往就够了。
8.4 低峰窗口:最后的兜底方案
不管用什么高级工具,一种最朴素、也最可靠的做法是:在业务低峰窗口执行DDL,配合监控和回滚方案。低峰不代表凌晨3点就万事大吉,要看你业务的真实情况,比如面向海外用户的系统,低峰期完全可能在白天。
执行前设置几个“闸门”:
-- 设置锁等待时间上限,避免DML被长时间阻塞 SET SESSION innodb_lock_wait_timeout = 5; -- 关闭非交互式超时 SET SESSION wait_timeout = 86400;并开一个监控脚本盯着SHOW PROCESSLIST,如果发现长时间的元数据锁等待,及时判断是哪个会话卡住了DDL。通常卡DDL的是老会话持有事务未提交,找到并处理掉即可。
9. 常见问题与排查技巧实录
这部分我罗列这几年做技术支持时最常碰到的DDL相关问题,做成速查表,你直接按图索骥。
9.1 报错速查表
| 报错信息 | 含义 | 解决方案 |
|---|---|---|
ERROR 1062 (23000): Duplicate entry | 唯一约束冲突 | 已有重复数据,先清理再建唯一索引 |
ERROR 1064 (42000): You have an error in your SQL syntax | 语法错误 | 检查关键字、逗号、引号,对照官方手册核对 |
ERROR 1071 (42000): Specified key was too long | 索引键过长 | 检查表字符集,utf8mb4下索引最大长度是3072字节(8.0 InnoDB),缩短索引字段长度或使用前缀索引 |
ERROR 1091 (42000): Can't DROP ... | 对象不存在 | 确认名称,注意索引名与约束名的区别 |
ERROR 1146 (42S02): Table doesn't exist | 表不存在 | 检查库名、表名,注意大小写和下划线 |
ERROR 1170 (42000): BLOB/TEXT column used in key specification without a key length | 对TEXT列建索引未指定长度 | 对TEXT列建索引必须加前缀长度 |
ERROR 1215 (HY000): Cannot add foreign key constraint | 外键添加失败 | 检查两张表的字段类型、字符集是否完全一致 |
ERROR 1822 (HY000): Failed to add the foreign key constraint | 缺少被引用列的索引 | 被引用的父表列必须有索引,通常是主键或唯一键 |
ERROR 3780 (HY000): Referencing column and referenced column don't match | 外键列与父表列不匹配 | 检查类型、长度、无符号属性、字符集与排序规则 |
9.2 线上卡死:都是MetaData Lock惹的祸
最经典的故障是:一条ALTER TABLE跑了一上午,卡住不动,所有写入都堵住了。原因往往是有老事务没结束,持有元数据锁。
场景还原:
- 一个服务用长连接连接数据库,开启事务后执行了一条查询,但代码里事务提交/回滚逻辑写得不严谨,事务一直没结束。
- 这时候执行
ALTER TABLE,它需要等待所有元数据锁释放。 - 于是
ALTER TABLE进入等待队列,后续所有DML也在等ALTER TABLE完成。 - 全库陷入“阻塞连环套”。
排查方式:
-- 查看所有进行中的事务 SELECT * FROM information_schema.innodb_trx\G -- 查看当前所有会话及状态 SHOW FULL PROCESSLIST;解决办法:找到持有锁的trx_mysql_thread_id,评估后KILL掉阻塞会话,DDL就能继续。
经验是:DDL之前先查事务和锁,别急着执行。
9.3 修改默认值不生效的乌龙
新手经常踩的坑:执行了ALTER TABLE t ALTER COLUMN c SET DEFAULT 100;,然后插入NULL发现还是NULL,问为什么默认值没生效。
搞清楚一个概念:默认值只在新插入且不指定该列值时生效,如果显式插入NULL,那写进表的就是NULL。如果想要“没值就自动填充”,业务代码要做IFNULL处理,或者建表时用DEFAULT加NOT NULL约束。
9.4 大表删除的幻觉
DROP TABLE删除一张1亿行的表,执行完也可能瞬间返回,但磁盘空间不一定立刻释放。InnoDB会先在数据字典里标记表已删除,真正的空间清理交给后台线程做,这个清理过程可能会占用IO。所以大表删除后,如果有监控发现磁盘空间没释放,先等等,不要重复操作。
如果你用的是MySQL 8.0,DROP TABLE整体空间管理会比5.7更平滑,因为表空间可以独立删除。如果还是担心空间碎片,可以考虑分区表策略,按时间或范围分片,业务上删除老数据时直接DROP PARTITION,效率高得多。
10. 一份可抄作业的DDL模板
查资料的最高效率是“拿来能用”。下面这两个模板基本覆盖了我日常开发中80%的场景,可以直接复制改一改。
业务表模板:
CREATE TABLE `order_info` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', `status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订单状态:0待支付 1已支付 2已发货 3已完成 4已取消', `total_amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额', `remark` VARCHAR(200) DEFAULT NULL COMMENT '备注', `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_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`), KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';通用改表模板(务必按顺序核对):
-- 1. 加字段 ALTER TABLE `table_name` ADD COLUMN `new_col` VARCHAR(50) DEFAULT NULL COMMENT '新字段' AFTER `some_col`; -- 2. 改字段类型 ALTER TABLE `table_name` MODIFY COLUMN `col` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '改后的注释'; -- 3. 改字段名和类型 ALTER TABLE `table_name` CHANGE COLUMN `old_col` `new_col` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '新注释'; -- 4. 加索引 ALTER TABLE `table_name` ADD KEY `idx_col` (`col`); -- 5. 加唯一约束 ALTER TABLE `table_name` ADD UNIQUE KEY `uk_col` (`col`); -- 6. 删索引 ALTER TABLE `table_name` DROP KEY `idx_col`; -- 7. 建表后补表注释 ALTER TABLE `table_name` COMMENT='表的注释';11. 我的几点体感建议
文章写到这儿,语法层面的东西基本覆盖了。最后说点这些年操盘DDL的体感。
第一,DDL不是“写一次就完事”的事,它是一个持续设计的动作。建表只是开始,随着数据量和业务的增长,你可能要加索引、优化字段、归档旧数据,每一项都是DDL的活。早点把这些语法吃透,比等到线上出故障再临时翻文档靠谱一万倍。
第二,不要迷信“Online DDL”四个字。ALGORITHM=INPLACE, LOCK=NONE确实能减少锁的影响,但InnoDB底层还是会占用额外空间、增加主从延迟。对超大表,该用工具用工具,该排窗口排窗口。工具不是万能的,但比裸写SQL裸跑安全得多。
第三,备份是DDL的最后一道防线。任何一次DROP、大范围ALTER、索引变更,操作前都做一次备份。备份不一定要恢复到生产,但一旦出现不可逆的错误,你至少还有退路。我做DDL时的习惯是:不管操作多简单,先mysqldump单表备份,或者用CREATE TABLE ... LIKE+INSERT SELECT做一个快速快照,成本不高,买个安心。
最后再分享一个实用小技巧:在测试环境对一张大表做ALTER TABLE时,可以先把它改成同结构的空表试跑一次,观察执行计划和锁等待情况。这不能精准预测生产环境的耗时,但至少能帮你排除语法和结构层面的低级错误。别问我怎么知道的,问就是当初被MODIFY的重置属性坑过不止一次。