原创

MySQL INSTANT 改表的边界:默认值、索引与元数据锁

给旧表加一个常量默认值列,ALTER 很快完成;接着给它建索引,同样写 ALGORITHM=INSTANT 却被拒绝。两次都叫“改表”,实际工作并不相同。前者可以通过元数据解释旧行,后者需要为现有记录建立索引项。

MySQL 8.4 的 INSTANT 是对特定操作及表状态的支持,不是把任意 ALTER 变快的开关。显式写出它,一个重要作用恰恰是:当前操作不能满足条件时失败,不悄悄退到另一条更重的执行路径。

加列不回填旧行,也能读到默认值

在专用实验库创建一张没有全文索引、使用普通 DYNAMIC 行格式的 InnoDB 表。后面的语句依次执行,别把故意失败的例子塞进遇错即退出的整段脚本。

CREATE DATABASE IF NOT EXISTS instant40_lab
  CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE instant40_lab;

CREATE TABLE work_item (
    id BIGINT UNSIGNED NOT NULL,
    status VARCHAR(16) NOT NULL,
    created_at DATETIME(6) NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

INSERT INTO work_item VALUES
(1,'NEW','2025-01-01 00:00:00'),
(2,'DONE','2025-01-02 00:00:00');

ALTER TABLE work_item
ADD COLUMN priority TINYINT UNSIGNED NOT NULL DEFAULT 0,
ALGORITHM=INSTANT;

SELECT id,status,priority FROM work_item ORDER BY id;

两条旧记录应读到 priority=0。这不要求执行器把每条历史行都更新一遍;InnoDB 可以根据行版本与列元数据,解释旧布局中尚不存在的字段。读取结果具备默认值,与物理上已经进行整表回填,是两种实现路径。

再修改插入默认值,并插入一条没有提供 priority 的新记录:

ALTER TABLE work_item ALTER COLUMN priority SET DEFAULT 5,
ALGORITHM=INSTANT;

INSERT INTO work_item (id,status,created_at)
VALUES (3,'NEW','2025-01-03 00:00:00');

SELECT id,status,priority FROM work_item ORDER BY id;

旧的1、2仍为0,新记录3使用5。改变默认值没有把所有历史记录改成5;这项规则也不会因为旧列最初是 INSTANT 添加而变成自动回填。业务若要求历史值改成另一个结果,需要单独定义数据迁移,而不是只改 DDL 后宣布完成。

给索引指定 INSTANT,是应该拒绝的请求

下面故意要求一个不支持的算法:

ALTER TABLE work_item ADD INDEX idx_status (status), ALGORITHM=INSTANT;

普通二级索引创建不属于这条 INSTANT 路线,应收到不支持相应算法的错误。接下来确认索引没有建立,再选择本表支持的在线索引创建方式:

SHOW INDEX FROM work_item;

ALTER TABLE work_item ADD INDEX idx_status (status),
ALGORITHM=INPLACE, LOCK=NONE;

SHOW INDEX FROM work_item;

在本例普通表条件下,这条二级索引创建支持并发 DML,但它仍要读取记录并构建索引。INPLACE 也不能按英文直觉解释成“不重建、不分配额外空间”:不同操作的工作量不同,某些 INPLACE 操作确实会重建表。必须查具体操作对应的支持矩阵。

如果一条 ALTER 同时包含“加普通列”和“加二级索引”,不能因为前一部分支持 INSTANT 就要求整条语句 INSTANT。选择拆开还是合并,要看希望限制算法、部署依赖与维护成本如何安排;拆开也意味着两次 DDL 边界,不会自动组成一个可回滚的业务事务。普通 ALTER TABLE 会隐式提交,不能与同连接里尚未提交的业务 DML 混在一起,指望最后统一撤回。

不扫描全表,仍然可能等元数据锁

另开连接A,保持一个读取该表的事务不结束:

USE instant40_lab;
START TRANSACTION;
SELECT id,status FROM work_item WHERE id=1;

连接B使用短一些的实验等待预算,并尝试再加一列:

USE instant40_lab;
SET @saved_lock_wait = @@SESSION.lock_wait_timeout;
SET SESSION lock_wait_timeout = 1;
ALTER TABLE work_item
ADD COLUMN reviewed TINYINT NOT NULL DEFAULT 0,
ALGORITHM=INSTANT;

连接A仍持有相应元数据锁时,B可能在等待后超时,不能据此认定服务器不支持 INSTANT。这个等待预算是元数据锁相关的 lock_wait_timeout,不是只调整 InnoDB 行锁等待的 innodb_lock_wait_timeout。

回到A执行 ROLLBACK,再在B重复同一条加列操作。成功后恢复B的会话参数:

SET SESSION lock_wait_timeout = @saved_lock_wait;
SHOW COLUMNS FROM work_item;

第一次失败以后,先确认列没有出现再重试;如果真实网络错误发生在提交响应阶段,应先读当前结构,不能看到客户端报错就无条件重复同名加列。这个实验把锁等待故意固定在专用表上,没有要求去终止任何业务连接。

INSTANT 只减少某些数据重写工作,并没有取消修改元数据需要协调的事实。线上长事务、连接池里没有结束的事务,都可能把一条看起来很轻的 DDL 挡住。验收时应分别记录算法拒绝、锁等待和结构最终状态,不能都归为“ALTER很慢”。

8.4 的条件,不能从别的版本直接抄过来

MySQL 8.4 支持在任意位置 INSTANT 添加普通列,但仍有行格式、全文索引、表类型、行大小和内部列数量等限制。例如 ROW_FORMAT=COMPRESSED、带 FULLTEXT 索引的表不能直接沿用本例加列操作;临时表也没有相同支持。拿一张空白测试表成功的结果,不能替另一张复杂表签字。

还要观察历次即时增删列留下的行版本。具备对应管理视图权限时,可以查询:

SELECT NAME,TOTAL_ROW_VERSIONS
FROM INFORMATION_SCHEMA.INNODB_TABLES
WHERE NAME='instant40_lab/work_item';

8.4 的行版本上限为64;这不是“整张表只能有64列”,也不是普通业务记录的版本号。达到该上限后,再请求相应 INSTANT 增删列会失败,需要评估表重建等后续处理。后续大版本可能有不同限额,例如9.1.0起的调整,不能倒套到8.4。

重建可以重置这项计数,但重建本身有成本。把每次细小迁移都拆成反复加列、删列试探,可能让测试环境和正式表逐渐具有不同历史。复现时除了 SHOW CREATE TABLE,还应记录相关元数据与实际服务器版本,不能假定两份相同列定义就拥有完全相同的即时变更余量。

有些改动看起来只是 VARCHAR 多容纳几个字符,实际上还要考虑字符集最大字节数和长度编码是否跨界;有些更改需要检查现有 NULL 或重新计算存储生成列。它们都不能通过统一追加 INSTANT 处理。迁移工具应把算法要求视为执行约束,在不支持时停下来,保留明确错误,交给操作方重新选择。

对于这张 work_item,实验应留下三项可以核对的结果:旧行默认值没有因后续默认更改而漂移,二级索引按支持的路径建成,等待锁失败后再确认结构并恢复。它们比记录一次“改表用了多少毫秒”更能说明这次迁移的边界。

正文到此结束
评论插件初始化中...
Loading...