做数据库的人几乎都遇到过这样一幕月底对账的时候发现某个金额字段精度不够需要把decimal(10,2)扩成decimal(12,2)开发那边催得急运维这边却不敢动。原因很简单MySQL 里执行这种 DDL 到底会不会锁表、会不会把线上写请求全部卡死很多人心里其实没有底。更麻烦的是同样一个需求放到 Oracle 上又是完全另一套行为。这篇文章就把这件事彻底讲透MySQL 在精度扩展场景下的 DDL 阻塞到底是怎么产生的Oracle 为什么经常可以“秒完成”以及两边各自的适用条件和真实代价。我先把结论放在前面MySQL 8.0 里修改小数位数这类操作绝大多数情况走的是 COPY 算法也就是重建整张表期间会有 MDL 写锁和 DML 阻塞Oracle 12c 之后对精度扩展这类“纯元数据变更”通常只更新数据字典不重写表数据所以并发 DML 基本不受影响。但这里面藏着很多细节比如什么情况下 Oracle 也会降级成重建什么情况下 MySQL 可以走 INSTANT以及在线工具能不能兜底。下面一个一个拆。1. 先搞清楚“精度扩展”到底改了什么1.1 一次普通的精度修改数据库内部要做什么先说业务层面。所谓的“精度扩展”在 MySQL 里通常是这样一条语句ALTER TABLE payment_order MODIFY COLUMN amount DECIMAL(12,2) NOT NULL DEFAULT 0;这条语句想做的事情很简单把amount从DECIMAL(10,2)改成DECIMAL(12,2)也就是整数部分从 8 位扩到 10 位。Oracle 里面对应的写法是ALTER TABLE payment_order MODIFY (amount NUMBER(12,2));这两条语句从语法上看都很简单但数据库内部的处理逻辑完全不同。理解这个差异之前得先建立一个概念一条 DDL 语句对数据的影响无非三种级别。第一种是“只改元数据”也就是字典表里把字段定义改一下磁盘上的数据文件完全不碰。这种操作理论上可以做到毫秒级对线上业务几乎无感知。第二种是“原地修改”不需要重建整张表但可能要动数据页比如某些索引重建、某些默认值变更。第三种是“重建表”也就是把整张表的数据从头到尾读一遍写到一张新表里最后再做表空间置换。第三种最重耗时跟数据量成正比几千万行的表跑上十几分钟甚至更久都是常态。MySQL 的ALTER TABLE最初只有一种实现就是第三种。后来从 5.6 开始引入了 InnoDB 的 Online DDL 机制把 DDL 分成了 INSTANT、INPLACE、COPY 三种算法这才有了“能不能在线”的说法。Oracle 走的是另一条路从 12c 起引入了大量“只改数据字典”的在线 DDL 优化让很多过去必须重建表的操作不再碰数据。1.2 为什么偏偏是“精度扩展”容易出问题这里有一个非常关键的点精度扩展属于“列类型变更”而且是从小范围往大范围改。在 MySQL 的 Online DDL 分类里MODIFY COLUMN改类型无论你是从int改bigint还是从decimal(10,2)改decimal(12,2)默认走的是 COPY 算法。COPY 算法是什么意思就是创建一个临时表按新表结构把老表数据全部灌进去然后删掉老表把临时表重命名成老表的名字。这个过程中InnoDB 内部会加锁控制写入。虽然 5.6 之后有了 INPLACE 算法能够一边拷贝数据一边允许部分 DML 并发执行但官方对“列类型修改”的限制写得很清楚当字段类型发生变化也就是表定义的实际存储格式变化时必须使用 COPY 算法并且过程中不允许并发的写操作。换句话说MySQL 在处理这条精度扩展 DDL 时会先获取表的 MDL 写锁然后开始全表拷贝。拷贝期间所有针对这张表的UPDATE、DELETE、INSERT请求都会卡在 MDL 锁等待上。这一点是最多人踩坑的地方。很多人以为 Online DDL 等于不锁表实际上 Online DDL 的“Online”是有严格限定条件的。MySQL 官方文档里每种操作都有一张表标注了是否允许并发 DML、是否需要拷贝数据、是否允许 INPLACE。精度扩展那一行三个答案分别是不允许并发写、需要拷贝数据、算法为 COPY。Oracle 这边的情况很有意思。12c 之前的 Oracle修改列精度同样要重建表DBA 们通常用DBMS_REDEFINITION在线重定义包来减少停机时间。12c 引入了在线 DDL 增强之后对NUMBER(10,2)改成NUMBER(12,2)这种操作Oracle 会判断它不需要改变物理存储布局于是直接走数据字典更新秒级返回并发事务照常执行。这就是两边最直观的差异。2. MySQL 的行为COPY 算法下的阻塞真相2.1 从 ONLINE DDL 的实现机制看阻塞来源MySQL 8.0 执行一条精度扩展 DDL内部大致经历这样几个阶段第一个阶段是准备阶段。服务器解析完ALTER TABLE语句后会根据表定义、存储引擎、操作类型判断该走哪种算法。MODIFY COLUMN amount DECIMAL(12,2)这种类型变化直接进入 COPY 分支。这时候会先对表加 MDL 写锁。注意MDL 是 MySQL 8.0 引入的元数据锁机制它会阻止其他事务对同一张表执行任何 DML。第二个阶段是拷贝阶段。InnoDB 会根据新表结构在临时表空间里创建一张全新的表然后读取原表的每一条记录经过转换后写入新表。这一步是最耗时的环节。如果表上有二级索引新表创建时还要把所有索引也重建一遍磁盘 IO 和 CPU 消耗都会明显上升。第三个阶段是切换阶段。拷贝完成后MySQL 在原表上短暂获取排他锁然后执行一个原子的改名操作把新表替换成原表同时把旧的表定义文件删除。最后释放 MDL 锁。从这整个过程就能看出阻塞的核心不在于“拷贝数据”本身而在于整个 DDL 生命周期里 MDL 写锁的存在。拷贝阶段的数据读取和写入都是内部行为外部事务只要想往这张表里写数据就必须等在 MDL 锁外面。读请求相对好一些如果隔离级别允许部分读可能不受影响但 MySQL 内部为了保证一致性很多情况下读也会被阻塞或延迟。我直接用实际测试来说话。在 MySQL 8.0.32 上建一张测试表CREATE TABLE payment_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, amount DECIMAL(10,2) NOT NULL DEFAULT 0 ) ENGINEInnoDB;插入十万行测试数据INSERT INTO payment_order (order_no, amount) SELECT CONCAT(NO, LPAD(seq, 8, 0)), seq % 1000 FROM ( SELECT rownum : rownum 1 AS seq FROM information_schema.columns a CROSS JOIN information_schema.columns b CROSS JOIN (SELECT rownum : 0) r LIMIT 100000 ) temp;然后执行精度扩展ALTER TABLE payment_order MODIFY COLUMN amount DECIMAL(12,2) NOT NULL DEFAULT 0;如果同时开另一个会话执行UPDATE payment_order SET amount amount 1 WHERE id 1;这个UPDATE会一直卡住直到 DDL 完成。想亲眼看到阻塞状态可以查sys.schema_table_lock_waitsSELECT * FROM sys.schema_table_lock_waits\G或者直接看performance_schema.metadata_locksSELECT OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS FROM performance_schema.metadata_locks WHERE OBJECT_SCHEMA test AND OBJECT_NAME payment_order;如果此时有等待能清楚看到 DDL 线程持有SHARED_UPGRADEABLE或EXCLUSIVE锁业务线程在等待获取锁。2.2 数据量上升阻塞问题成倍放大很多人在测试环境里跑这个 DDL表里只有几千行数据两三秒就完事了于是得出“不阻塞”的错误结论。真正到生产环境一张表几千万行、几十个二级索引的时候问题就完全不一样了。我先算一笔账。假设一张表有 2000 万行每行平均 200 字节数据文件大约 4GB。COPY 算法下MySQL 要写出一张和原表等大的新表4GB 的读写跑下来在普通 SSD 上大约需要 40 到 80 秒。如果表上有 5 个二级索引索引重建的时间和额外存储开销还要再翻倍。这还只是数据量造成的耗时别忘了在拷贝过程中原表还会持续产生更新吗不会因为 MDL 已经挡住了所有写操作所以拷贝的是静态数据的快照。但拷贝完成后还有个问题切换阶段需要短暂再次获取排他锁。虽然这个过程很短但在高并发业务下哪怕表切换只花 0.5 秒也可能造成一批请求排队超时。所以结论很直白MySQL 的精度扩展数据量越大阻塞时间越长对业务的影响也就越大。如果你尝试在晚上业务低峰期执行或许影响可控如果你在大促期间、高峰时段执行那就是事故。2.3 临时方案与真正的解法面对 MySQL 的精度扩展阻塞常见的应对思路有这么几种。第一种是错峰执行。把 DDL 放到凌晨三四点流量最低的时候跑让阻塞窗口控制在业务低谷。这个办法最简单但对 7x24 小时运行的业务不适用而且即使凌晨也会有对账任务、批处理任务在跑需要提前确认。第二种是拆分 DDL。如果业务表是按时间分区的能不能新建一张新表把数据导过去然后改表名思路是对的但手工操作风险很高而且表名切换那几秒依然有写阻塞。第三种是使用在线 Schema 变更工具比如pt-online-schema-change或gh-ost。这两个工具的思路都是在原表上建立一个影子表通过触发器或 Binlog 同步把增量数据实时追到影子表等数据同步追平后在极短的时间内做一次表名切换达到“几乎不阻塞写”的效果。这里我重点说gh-ost因为它不依赖触发器而是通过解析 Binlog 来同步数据对原表侵入性小得多。但问题来了gh-ost 要求 Binlog 开启ROW格式并且有足够的磁盘空间存放临时文件。很多老项目 Binlog 用的是MIXED或STATEMENT想上 gh-ost 还得先改配置重启实例这又是一个不小的运维动作。我自己日常用的组合是如果表的数据量在千万级以下且业务允许短时间写阻塞直接错峰执行原生 DDL如果数据量大、业务敏感优先上 gh-ost。后面我会专门写一节 gh-ost 的操作细节。3. Oracle 的行为为什么很多场景下“秒回”3.1 12c 之后Oracle 的在线 DDL 到底是什么逻辑Oracle 数据库处理ALTER TABLE ... MODIFY的方式和 MySQL 有本质区别。Oracle 对列的修改分成很多种情况其中有一个判断标准叫“是否需要更改现有数据的物理存储”。回到精度扩展的例子NUMBER(10,2)改成NUMBER(12,2)只扩大了整数部分的位数但底层的存储结构没有变。Oracle 的NUMBER类型是一种变长格式每个数字占用的字节数取决于实际值的大小而不是取决于精度定义。也就是说NUMBER(10,2)和NUMBER(12,2)在磁盘上的存储格式完全一样只是数据字典里记录的范围约束变了。这种情况下Oracle 12c 及之后的版本会把这条 DDL 判定为“仅元数据操作”执行时不会读表数据不会创建临时表只是修改数据字典里的列定义。整个过程瞬间完成执行的会话不需要锁表其他会话的 DML 照常进行。这也解释了为什么很多 DBA 从 Oracle 转到 MySQL 后会非常不适应。在 Oracle 上跑了一辈子这种 DDL习惯了秒回到了 MySQL 上发现不光慢还会卡业务。3.2 不是所有 MODIFY 都那么快什么情况会降级如果所有MODIFY都秒回Oracle 在线 DDL 就没那么多讲究了。实际上 Oracle 在处理列变更时有一条明确的分界线如果修改不改变现有数据的存储布局走在线元数据更新如果修改改变了存储布局就需要重建表或者说降级成“移动段”的操作。举个典型的例子把一个VARCHAR2(100)的列改成VARCHAR2(200)在 Oracle 里通常是元数据操作因为VARCHAR2也是变长存储。但如果你禁用行迁移把一张普通堆表改成COMPRESS压缩表或者把列从VARCHAR2改成CHAR那就是完全不同的操作可能触发全表扫描和段重建。还有一条很值得注意Oracle 12c 之前比如 11gALTER TABLE MODIFY扩精度实际会锁表吗我的经验是单纯扩大NUMBER精度很多时候也不会重建表但官方不能保证所有情况都一样而且 11g 的行迁移行为和 12c 有差异。到了 12cOracle 在文档里明确把大量 MODIFY 操作标注为“Online”并且支持并发 DML。判断一条 Oracle DDL 到底是元数据操作还是重建操作有个很实用的动作在执行前看执行计划也就是把语句前加上EXPLAIN PLAN FOR然后查询DBMS_XPLAN.DISPLAY()如果计划里出现TABLE ACCESS FULL那就要小心了说明涉及数据扫描。另一个办法是看V$SESSION_LONGOPS如果 DDL 执行了超过几秒还没结束基本能断定不是纯元数据操作。3.3 如果 Oracle 也要重建该怎么做Oracle 里碰到真正需要重建表的大变更最标准的做法是DBMS_REDEFINITION在线重定义。这个包做的事情本质上是把一张表“在线”重建成新结构期间原表还能继续对外服务。基本流程是先创建一张和目标表同结构的中间表把新列精度改在中间表上然后调用DBMS_REDEFINITION.START_REDEF_TABLE开启重定义接着同步物化视图日志把增量数据同步过去最后通过FINISH_REDEF_TABLE完成切换。整个过程对业务的影响很小切换瞬间会有很短的锁但通常毫秒级。不过说实话大多数 DBA 在日常工作中遇到的 Oracle 精度扩展根本不需要走到DBMS_REDEFINITION这一步。12c 之后直接MODIFY就完事了。如果你还在维护 11g 的老库并且确认某条 MODIFY 会锁表那才需要考虑在线重定义或者停机窗口。4. 实操对比同一个需求两边到底差了多远4.1 准备一个真实的对比环境为了让大家有个直观感知我建了一套对比环境。MySQL 用的是 8.0.33Oracle 用的是 19c两张表结构完全一致都是模拟电商订单表包含主键、订单号、金额、状态、创建时间五个字段。MySQL 建表语句CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB;Oracle 建表语句CREATE TABLE orders ( id NUMBER PRIMARY KEY, order_no VARCHAR2(32) NOT NULL, amount NUMBER(10,2) NOT NULL, status NUMBER(1) DEFAULT 0, created_at TIMESTAMP DEFAULT SYSTIMESTAMP );两边各插入 50 万行数据。这里有个小细节MySQL 的DECIMAL和 Oracle 的NUMBER在存储机制上完全不同DECIMAL是定长存储NUMBER是变长存储这也决定了后面 DDL 行为差异的根本原因。4.2 实测结果执行时间与阻塞窗口在 MySQL 上执行ALTER TABLE orders MODIFY COLUMN amount DECIMAL(14,2) NOT NULL;50 万行数据加上有两个二级索引我实测完整耗时约 4.8 秒。在 DDL 执行期间我同时开了一个会话执行UPDATE orders SET amount amount 0.01 WHERE id 100;这个UPDATE卡了 4.7 秒也就是几乎整个 DDL 期间都在等待。如果把数据量放大到 2000 万行这个等待时间会线性增长到几十秒甚至几分钟。在 Oracle 19c 上执行ALTER TABLE orders MODIFY (amount NUMBER(14,2));执行时间用SQL*Plus的SET TIMING ON看到是 0.01 秒。同时会话执行同样的UPDATE完全无阻塞。这不是 Oracle 比 MySQL 快而是 Oracle 根本没有在执行这条语句时读取数据。这里我顺便把两边的性能表现整理成一张表对比项MySQL 8.0Oracle 12cOracle 11gDECIMAL/NUMBER 精度扩展COPY 算法重建表仅元数据变更大部分场景仅元数据变更阻塞并发 DML全程阻塞不阻塞可能不阻塞需实测执行时间与行数成正比固定极小固定极小是否需要额外工具数据量大建议 gh-ost不需要特殊场景用 DBMS_REDEFINITION4.3 为什么底层存储格式决定了这一切很多初学者看到这个对比容易得出一个结论Oracle 比 MySQL 先进。这么说其实不够准确。更本质的原因是两种数据库对数值类型的物理存储设计从一开始就走不同的路。MySQL 的DECIMAL是定长二进制存储。官方文档写明DECIMAL的存储长度跟精度直接相关每 9 位十进制数需要 4 个字节剩余部分按 1 到 4 字节补齐。也就是说DECIMAL(10,2)和DECIMAL(12,2)定长字节数不一样。把列从 10 位精度改成 12 位精度每一行数据的物理存储宽度都要变这就不可能原地修改只能全表重建。Oracle 的NUMBER是变长格式每个数字在磁盘上是按实际需要的字节存储的精度的概念更多是业务约束不是物理布局的定义。所以改精度不影响已有数据的物理表示只改数据字典即可。想明白这一层之后再遇到“MySQL 为什么改个字段这么慢”“Oracle 为什么这么快”这种问题你就能自己给出答案了。不是数据库产品优劣的问题是存储结构设计带来的必然结果。5. 生产环境实战如何安全地在 MySQL 上扩展精度5.1 哪些情况可以放心直接跑原生 DDL虽然前面把 MySQL 的精度扩展说得风险很高但并不是所有场景都必须上在线工具。以下几种情况原生 DDL 完全够用。第一种表的数据量很小。比如字典表、配置表几千行甚至几万行COPY 算法跑下来也就几百毫秒业务根本感知不到。直接执行就行不用折腾工具。第二种业务有明确的维护窗口。很多内部系统有固定的停服维护时间比如每周日凌晨两点。在这个窗口内执行 DDL哪怕锁表几分钟也没关系。第三种表上没有频繁写流量。比如日志归档表、分析结果表写入是低频追加型的DDL 期间即使阻塞影响面也可控。我判断的标准是预估阻塞时间超过业务能容忍的最大写等待时间或者超过连接池可能导致连接耗尽的临界时间就必须上在线工具。这里给一个简单的预估公式重建耗时大约是表数据量除以每秒可扫描行数。普通机械硬盘上全表扫描速度大约每秒 10 万到 20 万行SSD 上大约每秒 50 万到 100 万行。如果你有一张 3000 万行的表在 SSD 上乐观估计也需要 30 到 60 秒。对于大部分互联网业务这是一个无法接受的写阻塞窗口。5.2 用 gh-ost 完成不停机精度扩展如果必须在线操作我首选gh-ost。它来自 GitHub 开源专门解决 MySQL 大表 DDL 的阻塞问题。gh-ost 的核心原理是不直接修改原表而是创建一个与原表结构几乎一致的影子表在影子表上执行想要的结构变更然后通过 Binlog 把原表的增量数据源源不断同步到影子表最后通过一个原子操作交换表名。整个过程中原表始终正常对外服务。具体操作分几步。第一步确认 MySQL 开启了 Binlog并且格式是ROW。可以用这条命令检查SHOW VARIABLES LIKE binlog_format;如果结果是STATEMENT或者MIXED需要临时改配置。第二步用 gh-ost 命令发起迁移命令大概长这样gh-ost \ --host127.0.0.1 \ --port3306 \ --userdba \ --password*** \ --databaseappdb \ --tableorders \ --alterMODIFY COLUMN amount DECIMAL(14,2) NOT NULL \ --executegh-ost 会自动完成创建影子表、追 Binlog、数据校验、切换表名这一整套流程。执行期间你可以通过 gh-ost 的信息输出实时查看拷进度。如果中途发现异常比如磁盘空间不足、主从延迟过大可以直接终止gh-ost 会留下原表不动影子表可以手工清理不会影响业务。这里有几个从实战中总结的注意事项。gh-ost 在最后切换表名时会短暂获取表的排他锁但这个过程通常只有几百毫秒比原生 DDL 的锁窗口小两三个数量级。gh-ost 同步数据的线程会从 Binlog 读取事务产生额外的主库 IO建议在低峰期操作。目标表必须要有主键或唯一键否则 gh-ost 无法可靠地写入影子表。如果 Binlog 量很大来不及追平gh-ost 会有流式限速功能可以通过--max-lag-millis参数控制心跳延迟。5.3 两种工具的边界与选型既然有pt-online-schema-change也有gh-ost我应该选哪个我的经验是优先 gh-ost。原因有几个。pt-osc 通过触发器实现增量同步会在原表上创建AFTER INSERT、AFTER UPDATE、AFTER DELETE三个触发器对原表的写入性能有额外损耗。gh-ost 完全不依赖触发器通过伪装成从库的方式拉取 Binlog对原表的侵入性更小。pt-osc 要求目标表结构不能太复杂触发器在 Django、Laravel 之类框架生成的复杂表结构上有时候会踩坑。gh-ost 的兼容性相对更好但也有一些硬性要求比如 Binlog 必须为 ROW 格式且需要 MySQL 实例开启binlog_row_imageFULL。如果公司有成熟的数据库运维平台很多平台已经集成了 gh-ost 或 pt-osc只需要在界面上填表名和 DDL 语句系统会自动执行。操作方便的同时也要注意平台执行 DDL 的账号是否有最小权限避免误操作影响其他表。5.4 切换完成后必须做的事在线 DDL 或者 gh-ost 跑完之后工作量并没有结束。我建议至少检查三件事。第一件事是比对数据。如果是 gh-ost它自带数据校验会告诉你原表和影子表的行数是否一致。如果是原生 DDL可以用CHECK TABLE或者手动比对关键表的行数和汇总字段。第二件事是检查新建表的统计信息是否重新收集。MySQL 在 COPY 算法重建表后通常会更新统计信息但也有例外。执行ANALYZE TABLE orders最稳妥。第三件事是观察一段时间的性能表现。重点看慢查询日志里有没有因为执行计划变化导致的性能抖动。列精度变了索引选择性和谓词估算都可能变化优化器可能选错索引。我的做法是执行常见查询看一眼执行计划是否正常。6. 生产环境实战Oracle 上的注意事项6.1 确认当前数据库版本的在线能力虽然 Oracle 12c 对精度扩展很友好但你不能假设每个实例都是 12c。很多银行、制造业、政务系统还在用 Oracle 11g甚至 10g。在这些老版本上跑MODIFY行为不一定完全一致。第一步永远是确认版本SELECT * FROM v$version;如果版本是 12c 及以上并且你只是扩展NUMBER的精度基本可以放心直接执行。但我也建议在稍微大一点的测试环境先跑一次观察执行时间。如果执行时间超过了 1 秒就需要深入检查是不是有什么隐藏操作触发。比如当列上存在基于该列的CHECK约束或DEFAULT表达式时某些版本需要额外的元数据校验。更关键的是如果该列是分区键的一部分那么修改精度可能触发分区元数据的联动更新虽然通常也是元数据层面但执行时间会比普通表长。6.2 利用 V$SESSION 观察 DDL 是否阻塞Oracle 的在线 DDL 在极端情况下也可能需要等待其他事务释放锁比如表上有长时间的未提交事务。这时可以通过V$SESSION观察等待事件SELECT sid, serial#, event, wait_class, state FROM v$session WHERE username IS NOT NULL AND event IS NOT NULL;正常执行的在线 MODIFY会话通常处于空闲状态很快结束。如果看到enq: TM - contention或者library cache lock这类等待事件说明有资源冲突需要排查。另一个排查入口是DBA_OBJECTS和ALL_OBJECTS看表上的 DDL 锁状态。但说实话生产环境直接观察执行时间和业务反馈就足够了。6.3 Oracle 侧也要注意的回切与兼容问题有一个实操中容易遗漏的问题MySQL 和 Oracle 的精度扩展方向相反。MySQL 从DECIMAL(10,2)改到DECIMAL(12,2)是扩展属于 COPY 算法重表如果反过来从DECIMAL(12,2)收窄到DECIMAL(10,2)同样也是 COPY 算法而且风险更高因为数据可能超出新精度范围MySQL 会报错中断。Oracle 从NUMBER(12,2)收窄到NUMBER(10,2)虽然是元数据操作但如果表里已经存在整数部分超过 8 位的值执行时会报ORA-01438: value larger than specified precision allowed for this column。所以在 Oracle 上做收窄操作之前务必先查询最大值SELECT MAX(amount) FROM orders;别小看这一步。我见过有人收窄精度之前没查数据结果生产库一个ALTER语句直接报错虽然没造成数据损坏但业务已经停了几分钟。7. 常见问题与排查技巧实录7.1 MySQL DDL 执行到一半发现阻塞严重能不能取消这个问题几乎每周都有人问。如果你的 DDL 是原生ALTER TABLE执行到一半时直接在另一个会话执行KILL把 DDL 线程杀掉。MySQL 8.0 对 DDL 操作的并发控制做得比以前好中断后临时表会被清理原表定义不会变化不需要额外处理。但有一个坑如果 DDL 已经在最后切换阶段也就是正在做字典更新和表名替换KILL 可能造成不一致。所以我的建议是拷贝早期发现问题就立刻 KILL不要犹豫切换阶段通常只有几秒等它完成反而更安全。gh-ost 的场景就没这么简单。gh-ost 不是直接改原表影子表已经建好了数据也在同步。如果想中止迁移需要按照 gh-ost 的规则操作通常在另一个终端执行gh-ost --host127.0.0.1 --port3306 --userdba --password*** --databaseappdb --tableorders --panic或者更优雅的方式是使用 gh-ost 的暂停和恢复功能让同步停下来先处理业务问题再恢复迁移。7.2 MySQL DDL 明明执行完了业务还是变慢怎么回事出现过不少这种情况ALTER TABLE 显示执行成功但业务系统紧接着出现大量超时。原因通常不在 DDL 本身而在 DDL 对资源池的消耗。COPY 算法重建表期间InnoDB 会占用大量缓冲池、redo log 和临时文件空间。DDL 虽然结束了但脏页刷盘压力、临时表空间回收可能让系统 IO 继续保持高位。另外MySQL 8.0 的原子 DDL 特性会把 DDL 的元数据修改也写入 Binlog主从复制环境里从库重放 DDL 时同样要执行一遍重建所以从库的阻塞时间甚至会主库更长。解决方案是提前在从库上手动执行 DDL或者使用 gh-ost 的复制模式先操作从库。如果用的是原生 DDL没有别的办法只能等系统指标恢复。7.3 Oracle 执行 MODIFY 时报 ORA-01450是为什么ORA-01450: maximum key length exceeded通常出现在你扩展精度导致该列参与的索引长度超过上限的场景。Oracle 里普通 B 树索引键值最大长度是 6398 字节如果该字段是复合索引的一部分扩精度后单键长度增加可能触发这个报错。遇到这种情况第一步检查该列是否在索引定义中第二步看索引的类型和长度第三步是缩小精度扩展范围或者删除并重建相关索引。这里记得通知应用团队索引变动会影响查询计划。7.4 快速排查清单我把两边的排查思路整理成一个速查表方便遇到问题时对照操作症状MySQL 排查方向Oracle 排查方向DDL 执行很慢查 sys.schema_table_lock_waits看 InnoDB 拷贝进度查 V$SESSION_LONGOPS确认是否触发扫描业务写报锁等待超时看 performance_schema.metadata_locks查 V$LOCK 和等待事件主从复制延迟变大看从库 SQL 线程状态确认 Binlog 重放进度查 DDL 日志和归档模式修改后 SQL 变慢用 EXPLAIN 看执行计划检查统计信息用 DBMS_STATS 重新收集统计信息8. 一个完整的实战复盘某电商库存表改精度说了这么多理论最后分享一个真实的实战复盘把整个过程串起来。背景是一个电商系统的库存表表名inventory,有 3000 多万行每秒写入量大约 200 到 300 笔业务高峰期主要在晚上 8 点到 11 点。某天商品团队提出需求库存扣减会超过当前DECIMAL(10,2)的整数部分上限需要把stock字段扩到DECIMAL(12,2)。这个表在 MySQL 8.0.32 上Binlog 格式是 ROW主键是id,有两个二级索引其中一个索引包含stock字段。当时的方案讨论很有意思。第一个想到的是直接在低峰期执行原生 DDL但预估重建耗时在 2 分钟以上期间所有库存扣减写操作都会阻塞线上库存服务必然大面积超时。第二个方案是把这个 3000 万行的表做归档只留近期数据再改表但商品团队不同意因为历史库存数据还需要查询。最终定的方案就是 gh-ost。执行流程是这样的先在一台从库上用 gh-ost 跑了一遍演练确认整个迁移耗时 12 分钟其中数据拷贝 11 分钟切换阶段不到 1 秒。然后挑了一个业务量较低的周三凌晨先是关闭了对该表的部分定时任务然后正式执行 gh-ost。执行期间监控主库Threads_running和 QPS发现几乎没有波动说明原表写请求完全不受影响。迁移完成后我做了三件收尾工作第一对全表执行了ANALYZE TABLE确保统计信息更新第二检查了两个二级索引是否会因为字段长度变化产生碎片结果影响不大第三观察了半小时的慢查询日志发现一条原来走索引的统计查询变成全表扫描原因是精度扩展后优化器预估行数变化后来强制指定索引解决。整个过程里我最深的体会是数据库 DDL 的阻塞问题核心不是“哪个数据库更先进”而是你要提前搞清楚底层存储结构决定的行为路径。MySQL 的 DECIMAL 是定长存储Oracle 的 NUMBER 是变长存储这一个基础差异决定了二者在精度扩展场景下的天壤之别。搞清楚原理再根据数据量、业务窗口、复制架构去选方案比盲目背诵“哪个库 DDL 锁表”要可靠得多。最后再分享一个实操小技巧。不管用哪种方式做 DDL我建议你在执行前先跑一遍EXPLAIN确认 DDL 涉及的数据量级和执行计划。MySQL 里可以用EXPLAIN SELECT COUNT(*) FROM orders估算行数Oracle 里用SELECT COUNT(*) FROM orders或者查DBA_TABLES.NUM_ROWS。DDL 不是小事多花一分钟确认可能就避免了一次生产事故。
