多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MySQL DDL避坑指南:从建表异常到锁表事故的实战复盘

MySQL DDL避坑指南:从建表异常到锁表事故的实战复盘 先说我今天为什么想写这个。上周三下午运维突然在群里甩了一张截图一条CREATE TABLE报ERROR 1071索引键长度超限。写这条语句的兄弟很委屈他只是在几个varchar(255)上加了个普通联合索引没想到 MySQL 直接不让建。我一看表结构——默认字符集是 utf8mb4单列 255 个字符按最多 4 字节存就是 1020 字节三列一加正好超过 InnoDB 3072 字节的索引上限。这不是他一个人的问题身边至少有一半人写 DDL 靠感觉报错才去查资料。MySQL 日常开发里大家聊得最多的是查询优化、事务隔离、索引选择反而库和表的 DDL 被当成入门操作草草带过。可真等到线上环境里加字段卡了半小时、一条ALTER TABLE把业务拖到雪崩、跨库 JOIN 时两张表字符集对不上你才会意识到 DDL 才是整个 MySQL 体系里最不该凭感觉写的内容。这篇我不打算念官方文档就按我实际维护库表、处理线上事故的经验把库级和表级的 DDL 核心操作、背后原理、踩坑链路一次说透。适合已经写过一阵子 SQL、懂基础语法但没系统处理过库表结构变更的读者。1. 从一个建表异常聊起DDL 为什么值得系统学那个 1071 报错本质上不是语句有问题而是建表的人没理解DDL 执行时 MySQL 会做哪些隐式检查。你以为只是写了个 CREATE实际上 MySQL 在后台至少做了三件事校验所有字段类型和长度、计算每一列的字节占用、再检查索引长度是否超过当前行格式和版本的限制。这个特点贯穿所有 DDL 操作。CREATE、ALTER、DROP、TRUNCATE看似关键字不同但它们都要先经过一层元数据锁MDL的排队再决定是直接改数据字典还是重建物理文件。理解了这层逻辑后面再聊大表变更、锁表事故你就能顺着机制自己推演而不是每次出了事才上网搜命令。再举个更常见的例子。很多团队建表习惯这样写CREATE TABLE user_log ( id INT PRIMARY KEY AUTO_INCREMENT, content VARCHAR(500), created_at DATETIME );这条语句在 MySQL 5.7 默认配置下可能能建出来但到了 8.0 某些版本组合下联合索引、utf8mb4字符集、外键关系一掺和就会出现各种奇怪报错。我见过最典型的是ERROR 1118行大小超过 65535 字节。有人把所有字段都定义成varchar(1000)逻辑上看着没问题但 MySQL 计算行的字节上限时不会按实际存储算而是按最大可能占用算。ERROR 1170试图把TEXT/BL O B字段直接做主键或索引不指定前缀长度。ERROR 1005建表时外键关联失败查下来往往是两个关联字段的字符集、类型或长度不一致。这些坑单看语法文档都能查到但真正难的是把它们串起来。你只有把库级 DDL 和表级 DDL 当成一个整体来对待知道字符集如何影响索引长度、索引长度如何约束字段选择、字段选择又如何决定ALTER的代价才能在写 DDL 的第一秒就避开大半故障。2. 库级 DDL 操作建库、改库、删库背后的四个关键决策很多人以为建库就是CREATE DATABASE test;一条命令的事。真实生产环境里建库前需要决策的点至少有四个字符集、排序规则、默认引擎、安全策略。这四个点一旦定错后面改起来比建一个新库还麻烦。2.1 建库时真正需要决策的四个点先看一条完整可用的建库语句CREATE DATABASE order_center DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT ENCRYPTIONN;第一字符集。我直接给结论新项目一律utf8mb4不要再用utf8mb3也就是大家常说的 utf8或latin1。utf8mb4是真正的四字节 UTF-8能完整覆盖 emoji、生僻字utf8mb3虽能省一点空间但实际业务里迟早会碰到字符存不进去的问题。MySQL 8.0 的默认字符集已经是utf8mb4但 5.7 及以下默认还是latin1这也是很多老项目迁库时中文乱码的根源。字符集和索引长度直接相关。InnoDB 在常见行格式DYNAMIC下索引最大 3072 字节utf8mb4一个字符最多 4 字节所以varchar(255)单列索引最多占 1020 字节三列联合索引就容易撞上限。这也是建表异常的高发区。第二排序规则。字符集之后的COLLATE决定了字符串比较和排序方式。常用的是这几种排序规则特点使用建议utf8mb4_0900_ai_ciMySQL 8.0 默认基于 Unicode 9.0不区分重音和大小写新项目首选utf8mb4_general_ci老版本常用比较快但规则不如 0900 精确兼容旧库时使用utf8mb4_bin区分大小写按二进制比较需要区分大小写时使用utf8mb4_unicode_ci老规则兼容性一般迁移特殊场景使用团队内部一定要统一排序规则否则两张表 JOIN 或 UNION 时容易报Illegal mix of collations这也是跨库 JOIN 最常见的坑之一。第三默认引擎。不用显式写在建库语句里但要确认default_storage_engineInnoDB。有些老项目或从 MyISAM 迁过来的环境建表时不定引擎就会建出 MyISAM 表事务、行锁、崩溃恢复全部失效。第四加密属性。MySQL 8.0 支持DEFAULT ENCRYPTIONN数据加密涉及性能开销我一般建议先关掉等有合规要求再统一评估开启。这里特别提醒一句ENCRYPTION只影响新表不会自动加密存量表。2.2 ALTER DATABASE改了库默认属性存量表不受影响库级修改没有表级那么常用但也需要说清楚。比如你想把某个库的默认字符集改成 utf8mb4ALTER DATABASE order_center CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;这条命令只修改数据库层的默认值已经存在的表不会跟着改。表级的字符集优先级高于库级每张表在建表时就把字符集固化在了元数据里。真要改存量表的字符集只能逐表执行ALTER TABLE order_main CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;注意CONVERT TO会重新编码所有字符列的数据大表执行时会重建表耗时和锁影响都要单独评估。如果想只改表的默认值、不动已有数据则用DEFAULT CHARACTER SET的写法ALTER TABLE order_main DEFAULT CHARACTER SET utf8mb4;这两种写法的区别我在生产环境里反复跟人强调过前者动数据后者只动默认属性。新手特别容易在这条语句上翻车把大表CONVERT当成小事执行。2.3 DROP DATABASE一条命令背后的风险DROP DATABASE比DROP TABLE更危险它会连带删除库下所有表和数据文件。更隐蔽的问题是它在 binlog 中会记录为一条DROP DATABASE语句如果是主从架构主库删了从库也会执行如果开启了 GTID这条 DDL 涉及的事务会直接改变复制状态恢复只能靠备份MySQL 本身没有类似回收站的机制。我在实际运维中养成的习惯是生产环境把DROP DATABASE权限收掉日常回收库用改名归档的方式比如把旧库RENAME成order_center_20250101_bak观察一段时间确认无业务访问后再走审批删除。RENAME DATABASE这个语法 MySQL 现在还不支持所以通常是把库内所有表搬到一个带_bak_前缀的库里或者干脆改表前缀。2.4 库级 DDL 里最容易被忽略的两个配置第一个是lower_case_table_names。这个参数决定库名、表名的字母是否区分大小写。Linux 上常见配置是 0区分大小写、1不区分、存储为小写、2存储原样、比较时忽略大小写。建库前如果不确认这个参数以后上线脚本在测试环境是小写表名、生产环境是大写表名就会到处报Table doesnt exist。第二个是character_set_server。很多人的建库语句不写DEFAULT CHARACTER SETMySQL 就会用这个服务器级参数。我见过一台 5.7 实例没改配置character_set_serverlatin1程序连接时以为连的是 utf8结果中文全部显示成问号。排查到最后发现根因在服务器默认值。所以我的经验是建库语句永远显式写明字符集和排序规则不依赖任何默认值。3. 表级 DDL从建表到改表的完整操作拆解表级 DDL 是日常开发里接触最多的部分。这里我不只列语法而是把每条操作的代价讲清楚因为ALTER TABLE什么操作廉价、什么操作昂贵直接决定了你能不能安全地在线上执行。3.1 建表时的字段类型、约束与索引顺序一段比较标准的生产建表语句长这样CREATE TABLE user_account ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键ID, login_name varchar(64) NOT NULL COMMENT 登录名, nick_name varchar(64) NOT NULL DEFAULT COMMENT 昵称, gender tinyint NOT NULL DEFAULT 0 COMMENT 性别 0未知 1男 2女, balance decimal(12,2) NOT NULL DEFAULT 0.00 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_login_name (login_name), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT用户账户表;几个容易出问题的细节主键用bigint unsigned自增。INT上限 21 亿很多业务不出三年就撞顶bigint可以撑很久。unsigned能把可用范围扩大一倍。金额用DECIMAL绝不用FLOAT或DOUBLE。浮点数是近似存储金额计算会出现0.10.20.30000000000000004之类的问题。这个属于基础共识但我在实际表结构评审里仍然经常看到有人用double存金额。DATETIMEvsTIMESTAMP。业务表我优先DATETIME因为TIMESTAMP只有 2038 年问题且受时区影响国际化业务很容易踩坑。状态字段用TINYINT不要用VARCHAR存 0/1/2。虽然可读性差一点但存储和索引效率高配合注释文档足够。联合索引的列顺序。KEY idx_status_created (status, created_at)把区分度高的字段放前面还是放后面的问题在查询侧更明显。但建表时至少要有这个意识等值条件字段放最左排序字段次之。这里也顺带提一个热搜词辅助索引如何避免回表——辅助索引里存的是主键值查询如果只需要索引内的列就覆盖避免回表需要其他列时MySQL会拿着主键去聚簇索引再查一次。所以太长的TEXT列别塞进辅助索引前缀索引虽省空间但也可能丧失覆盖能力。3.2 ALTER TABLE 的六种改装动作与代价线上执行最多的ALTER TABLE操作我总结成六类操作示例是否需要重建表加列ADD COLUMNMySQL 8.0 末尾加列可走 INSTANT8.0 之前多数需重建删列DROP COLUMN需要 INPLACE 重建表改列类型MODIFY COLUMN/CHANGE COLUMN几乎都要重建表且可能锁写改名RENAME COLUMN/RENAME TABLE只改元数据很快加索引ADD INDEX需要扫描全表并排序INPLACE耗时较长改字符集CONVERT TO CHARACTER SET全表重写代价高举一个典型的组合操作。业务要加一个最后登录时间同时把nick_name从varchar(32)加长到varchar(64)ALTER TABLE user_account ADD COLUMN last_login_at datetime NULL DEFAULT NULL COMMENT 最后登录时间, MODIFY COLUMN nick_name varchar(64) NOT NULL DEFAULT COMMENT 昵称, ADD KEY idx_last_login (last_login_at);注意 MySQL 的ALTER TABLE可以合并多条子句引擎会自动判断整体执行方案。合并的好处是减少扫描次数但坏处是如果其中某条子句是高代价操作整条语句都会被拖进高代价模式。所以线上变更我一般建议把加列和改类型拆成两条语句分开执行各自评估风险。MODIFY COLUMN有个特别容易踩的坑如果你只写字段类型不重新写NOT NULL、默认值、注释这些属性会变回默认状态。很多人执行完才发现默认值丢了、注释没了。比如ALTER TABLE user_account MODIFY COLUMN nick_name varchar(64);这条语句执行后NOT NULL约束没了DEFAULT 也没了COMMENT 昵称也没了。正确做法是把完整定义重新写一遍ALTER TABLE user_account MODIFY COLUMN nick_name varchar(64) NOT NULL DEFAULT COMMENT 昵称;3.3 在线 DDL 的底层逻辑INSTANT / INPLACE / COPYMySQL 5.6 引入在线 DDL5.7 完善8.0 又加入了更激进的 INSTANT 算法。这三个算法要分清楚COPY最古老的方式。创建一张新表把老数据逐行拷进去期间基本不允许并发写。任何版本的 MySQL 都支持但代价最大等于是重建表。INPLACE不需要完整复制数据文件到临时表但仍可能需要在原表上重建聚簇索引会有一段持有排他 MDL 锁的时间。加索引、加列旧版、删除列等大多走这个。INSTANTMySQL 8.0 引入只在元数据里做修改不需要重建表执行几乎是瞬时的。目前支持在表末尾加列有可空或默认值、加/删索引部分情况、改列默认值等。明确指定算法的写法ALTER TABLE user_account ADD COLUMN register_ip varchar(45) NULL DEFAULT NULL, ALGORITHMINSTANT;假设你的 MySQL 版本是 8.0.29 以上末尾加一个可空列走 INSTANT 就是秒级完成。但如果你是 5.7没有 INSTANT同样一条加列语句实际执行时会重建表表如果几个 GB执行时间就是几分钟起步期间LOCKNONE只保证不阻塞正常业务写不代表没有代价。所以我的核心建议是先确认版本再确认操作类型最后确定算法。不要只看支持在线 DDL就放心跑。3.4 几千万行大表的字段添加实战几个 GB 甚至几十 GB 的大表日常加字段怎么做才安全结合热搜词几千万行大表我分享一次实际操作。我当时要在订单流水表约 1.2 亿行表占用 40GB上加一个可空的src_channel字段。当时的版本是 MySQL 5.7没有 INSTANTADD COLUMN走 INPLACE会复制全表数据。直接执行实测锁等待风险很高我的应对方案是先看MAX(ctid)或者估算表大小确认变更窗口。选择凌晨低峰期。用pt-online-schema-change来做而不是原生ALTER TABLE。pt-online-schema-change的工作原理是创建一张临时表把原表的全量数据分批拷贝过去在这期间原表照常读写拷完后在切换点短暂锁表然后改名替换。它最大的优势是进度可控、可暂停、对业务影响小。pt-online-schema-change Dorder_center,torder_flow \ --alter ADD COLUMN src_channel varchar(32) NULL DEFAULT NULL COMMENT 来源渠道 \ --hostxx --userxx --passwordxx \ --chunk-size1000 --max-lag5 --execute执行过程中我用SHOW PROCESSLIST观察拷贝线程占用不高业务读写基本无感。切换窗口大约只有几百毫秒。MySQL 8.0.29 之后遇到末尾加可空列这种场景原生ALTER TABLE ... ALGORITHMINSTANT就够了不用上工具。但加索引、改列类型这类操作工具仍然比原生稳妥。结论一句话大表 DDL 不是能不能跑的问题而是怎么把影响降到可接受范围的问题。4. 锁表问题与 DDL 的冲突现场一次生产事故复盘搜索热词里反复出现mysql锁表说明锁和 DDL 的冲突是大家最容易踩的雷。这一章拿我之前处理过的一次真实故障来说。4.1 MySQL 锁的分类DDL 卡住的真正元凶MySQL 里和 DDL 强相关的锁主要有这几种锁类型说明与 DDL 的关系元数据锁 MDL保护表结构定义DDL 需要 MDL 写锁任何查询需要 MDL 读锁表级锁整张表加锁MyISAM 常见DDL COPY 期间会持有行级锁InnoDB 事务锁和 DDL 本身关系不大但长事务会阻塞 MDL意向锁表示表内存在行锁自动维护一般不用关心很多人觉得 DDL 卡住是表锁导致的其实大多数情况是长事务持有 MDL 读锁导致 DDL 拿不到 MDL 写锁。你写了一条ALTER TABLE它要等所有正在执行的查询和事务结束后才能拿到写锁。如果有某个连接开启了一个事务、读了一张表然后一直不提交、不关闭它手里的 MDL 读锁就一直不释放。这时候ALTER TABLE只能排队。更麻烦的是ALTER TABLE一旦排队后续对这张表的任何新查询也会排队形成连锁堆积最终表现为表被锁死。4.2 当时我看到的现场和排查链路事故表现是业务方反馈某核心接口超时数据库 CPU 不高、磁盘 IO 不高但连接数持续上涨大量 SQL 卡在Waiting for table metadata lock。排查过程我按这个顺序来SHOW PROCESSLIST;看有没有线程卡在Waiting for metadata lock记录它的Id。往上找有一条ALTER TABLE卡住了进程时间已经 21 分钟。在information_schema.metadata_locks中找持有锁的线程SELECT * FROM information_schema.metadata_locks WHERE OBJECT_SCHEMAorder_center AND OBJECT_NAMEorder_flow;顺着THREAD_ID找到持有锁的连接发现是一个空闲了很久的事务trx_state状态是RUNNING但trx_query是NULL说明它已经开了事务、读完表却没提交。最终处理把那个长事务的连接KILL掉ALTER TABLE立刻继续执行接口逐步恢复。根因是业务代码里用连接池某条逻辑开启事务后抛了异常没回滚、没关闭连接导致事务一直存活。4.3 规避锁表的三个实操方案这条事故之后我给自己定了几条规矩执行 DDL 前先查长事务SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx WHERE trx_stateRUNNING AND trx_started NOW() - INTERVAL 5 MINUTE;看到超过 5 分钟的长事务就要警惕。能通知业务提交就通知通知不了的评估是否KILL。lock_wait_timeout调到短一点比如 30 秒。不能让 DDL 无限期排队否则雪崩就是必然的。大表变更一律用pt-online-schema-change或gh-ost。它们内部会把 MDL 锁的持有时间压缩到极短不需要等待所有老事务结束。还有一个容易被忽视的mysqldump也会持有 MDL 读锁。备份高峰期执行 DDL一样会被堵。所以备份和 DDL 要错峰。5. 建表异常的完整排查链路从报错代码到根因这一章把建表过程中最常见的异常系统化整理一遍。很多时候报错只是一个信号真正要解决的是根因。5.1 常见建表异常清单报错代码典型信息高频根因1005Cant create table外键约束失败关联字段类型/长度/字符集不一致1050Table already exists目标表已存在未用IF NOT EXISTS1062Duplicate entry建唯一索引时已存在重复数据1071Specified key was too long索引字节长度超过 3072 上限1118Row size too large行字节占用超过 655351170BLOB/TEXT used without key length给TEXT/BLOB建索引未指定前缀长度1364Field doesnt have a default valueNO_ZERO_DATE等 SQL 模式或约束冲突严格模式下NOT NULL无默认值但插入空值1213Deadlock found多表外键或并发操作冲突多出现在插入阶段这里重点说最容易被忽略的1005 外键约束失败。当年我建一张订单表和用户表的外键语句写了两次都没成功报 1005。查SHOW ENGINE INNODB STATUS里的LATEST FOREIGN KEY ERROR发现user_account.id是bigint unsigned而订单表里的user_id写成了bigint类型长度一致但unsigned 属性不一致导致外键无法匹配。所以检查外键问题时不只要看类型还要看unsigned、字符集。5.2 一次索引超长的根因定位过程回到开头的 1071 案例。建表语句大致是CREATE TABLE visit_trace ( id bigint NOT NULL AUTO_INCREMENT, app_id varchar(64) NOT NULL, device_id varchar(128) NOT NULL, page_url varchar(255) NOT NULL, origin varchar(255) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_device_page (device_id, page_url, origin) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三个 varchar 在 utf8mb4 下的最大字节数是128×4 255×4 255×4 2552其实没超过 3072。但如果device_id是 255三个 255 就是 3060逼近上限只多一点点或再加一个字段就爆了。实际那个同事的报错是因为他把app_id、device_id、page_url三个字段都建成了 255总字节 3060加上 InnoDB 每列还有额外的长度记录开销直接超限。解决思路有三种缩短字段长度按业务实际值域来比如设备 ID 128 足够了URL 用 2048 存到TEXT单独列不建索引。改用前缀索引比如UNIQUE KEY uk_device_page (device_id(64),page_url(191),origin(64))。注意唯一约束用前缀索引后唯一性保障的是前缀而非完整值业务上要自己评估。把索引列改成utf8mb3单字符最大 3 字节能省四分之一空间。但前提是业务不需要 emoji。查这类问题最快的工具是information_schema.statisticsSELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX FROM information_schema.statistics WHERE TABLE_SCHEMAorder_center;再配合INFORMATION_SCHEMA.COLUMNS里的CHARACTER_OCTET_LENGTH字段可以直接算出每列的最大字节数。5.3 用 information_schema 排除字符集不一致我处理过不少跨表 JOIN 报Illegal mix of collations的问题根因就是建表时一张表用utf8mb4_general_ci另一张用utf8mb4_0900_ai_ci。排查 SQLSELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMAorder_center AND TABLE_NAME IN (user_account, order_main) AND COLUMN_NAME IN (login_name, user_name);看到两列字符集或排序规则不同直接CONVERT统一即可。这就是 DDL 里隐性成本的典型体现——建表时一个字符集参数没对齐后面所有关联查询都可能踩坑。6. DDL 与周边生态的衔接存储过程、跨库操作与权限DDL 不是孤立技术它和存储过程、跨库查询、权限体系这三块经常交织在一起。6.1 存储过程中的 DDL动态 SQL 与预处理陷阱存储过程里直接写 DDLMySQL 的行为和普通 SQL 不太一样。比如你直接写CREATE PROCEDURE p_create_table() BEGIN ALTER TABLE user_account ADD COLUMN status2 TINYINT NULL; END;在 MySQL 8.0 里这条 DDL 在存储过程中是不允许的会直接报错This version of MySQL doesnt yet support ALTER TABLE in stored procedure。如果需要动态执行必须用PREPARE加EXECUTECREATE PROCEDURE p_add_column(IN tbl VARCHAR(64)) BEGIN SET sql CONCAT(ALTER TABLE , tbl, ADD COLUMN tmp_col INT NULL); PREPARE stmt FROM sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; END;注意这里tbl来自参数存在 SQL 注入风险不要直接把用户输入拼进去最好白名单校验。另外存储过程里执行 DDL 会导致隐式提交会打断当前事务这也是很多人没料到的副作用。6.2 跨库 JOIN 与库表归属的 DDL 规划很多团队初期把所有表建在一个库里业务膨胀后开始按域拆库。跨库 JOIN 在 MySQL 里是允许的写法就是完整的库名.表名SELECT a.pay_amount, b.nick_name FROM order_center.pay_log a JOIN user_center.user_account b ON a.user_id b.id;但这给 DDL 带来的新要求是关联字段的字符集、排序规则必须一致否则报错涉及跨库查询时需要同时授予两个库的权限库之间表名可能冲突命名规划要全局统一。我见过最头疼的场景是基础数据表放在 A 库业务表放在 B 库两边 DBA 独立管理A 库表加了字段B 库的存储过程没跟上跨库 JOIN 的结果直接少列连报错都没有。所以我一直建议拆分库之前先梳理清楚哪些表会被跨库 JOIN尽量把强关联的表留在同一个库实在要跨就要有统一的 DDL 变更通知机制。6.3 DDL 权限控制最小授权原则生产环境里普通开发账号不该持有任意表的ALTER、DROP、CREATE权限。MySQL 8.0 支持更细的权限控制例如只允许在指定库建表、改表但不允许删库GRANT CREATE, ALTER, INDEX, DROP ON order_center.* TO app_ddl%;我日常维护时会把账号分成几层只读账号SELECT业务账号SELECT, INSERT, UPDATE, DELETE变更账号额外ALTER, CREATE, DROP, INDEX只能在变更窗口使用管理账号全局权限仅 DBA 持有这样即使某条 SQL 注入或者误操作影响面也是可控的。DDL 属于破坏性最强的操作类型权限收敛是最后一道防线。7. 实战总结我的 DDL 操作检查清单压箱底的东西放最后。下面这套清单是我踩过无数坑后沉淀下来的每次上线表结构变更都要过一遍。7.1 线上变更前的检查清单确认版本SELECT VERSION();看是否支持 INSTANT决定用原生 DDL 还是工具。确认表大小SELECT table_name, ROUND((table_rows*avg_row_length)/1024/1024,2) AS size_mb FROM information_schema.tables WHERE table_schemaorder_center AND table_nameorder_flow;确认长事务查information_schema.innodb_trx超过 5 分钟的事务提前处理。确认备份DDL 前至少要有最近一次全量备份大表变更建议再导一份表结构。选择低峰期凌晨 0 点到 6 点是首选。评估锁影响判判断ALTER会持有多久 MDL 写锁。如果预估超过一秒优先考虑pt-online-schema-change。准备回滚方案加字段这类操作一般不回滚等字段上线后确认无问题再做清理删字段必须提前备份数据。7.2 日常运维里的几个习惯习惯一所有 DDL 都走变更单。不管语句多简单DROP TABLE和TRUNCATE TABLE必须双人复核这是铁律。习惯二大表变更前先做一次试跑。在测试库导入同样结构、哪怕数据量小一个量级的表执行同一条 DDL记录耗时估算线上耗时。习惯三监控 DDL 进度。在另一个会话里反复执行SHOW PROCESSLIST;看状态是copy to tmp table还是alter table能感知是否卡住。用gh-ost或pt-osc看重放进度更方便。习惯四变更后验证元数据。用SHOW CREATE TABLE确认字段、索引、注释、字符集都符合预期不要只看结果成功就算完。习惯五留下变更记录。把每次 DDL 的语句、执行时间、耗时、表行数变化都记到运维文档里。一个月后想回看这张表为什么有这么多索引有一份记录会省太多事。最后再分享一个让我印象很深的小技巧所有生产环境的库表变更我在执行前都会先跑一遍EXPLAIN或者SHOW CREATE TABLE确认要变更的表确实是目标表。听起来很蠢但真的有人在调试环境里跑了半天ALTER最后发现改的是隔壁库的同名表。库表 DDL 是对数据库直接动手的操作所有我以为都会付出时间代价。稳妥一点慢一点才是这行的生存之道。
返回列表