在线 DDL 原理与最佳实践:MySQL/PostgreSQL 大表变更的锁与数据拷贝

对千万级、亿级大表执行 ALTER TABLE 是最让 DBA 与后端工程师胆战心惊的操作之一:一个看似简单的加列、加索引,可能把业务写流量堵死几分钟,甚至引发锁等待风暴。问题的根源在于表结构变更需要在短时间内独占表锁、重写整张表。为此,数据库演进出了在线 DDL 机制(MySQL 的 InnoDB Online …

对千万级、亿级大表执行 ALTER TABLE 是最让 DBA 与后端工程师胆战心惊的操作之一:一个看似简单的加列、加索引,可能把业务写流量堵死几分钟,甚至引发锁等待风暴。问题的根源在于表结构变更需要在短时间内独占表锁、重写整张表。为此,数据库演进出了在线 DDL 机制(MySQL 的 InnoDB Online DDL、PostgreSQL 的 ALTER 重建),以及更激进的第三方方案(pt-online-schema-change、gh-ost)。本指南从原理出发,讲清在线 DDL 的锁模型、数据拷贝策略、MySQL/PostgreSQL 各自的限制,并给出大表变更的完整最佳实践与应急预案。

一、为什么 DDL 会阻塞

1.1 元数据锁(MDL)与表重建

MySQL 表结构变更的两种成本:
  1. MDL(元数据锁):
     任何 DDL 先要拿到 MDL 写锁
     → 期间阻塞所有读写请求
  2. 数据拷贝:
     变更需要重建表(如加列且非原地、改类型、加索引)
     → 逐行 COPY 旧数据到新表,期间持续占用资源

两种成本叠加 = 大表 DDL 慢且阻塞

历史教训:
  · MySQL <5.6:ALTER 全程持锁(表级写锁),大表变更≈宕机
  · MySQL 5.6+:Online DDL 支持,部分操作不再全程持锁
  · 但"拷贝行数据"阶段的锁仍需控制

ℹ️ 核心矛盾:在线 DDL 要做的是"在不长时间独占写锁的前提下完成结构变更"——要么原地改(Instant/In-place),要么后台慢慢重建、锁只短暂持有。

1.2 三类变更算法

① INSTANT(MySQL 8.0.12+):
   只改元数据,不动行数据
   例:加列(列在末尾、无默认值外)、改列默认值
   速度:毫秒级,零阻塞

② INPLACE(原地重建):
   在同一表空间中重建,期间可并发读写
   例:加普通索引、加/删列(部分)、修改 varchar 长度(同空间)

③ COPY(拷贝表):
   创建新表 + 拷贝全部数据 + 原子替换
   例:改主键、改字符集、压缩表
   期间按锁策略控制读写并发

二、MySQL InnoDB Online DDL 详解

2.1 Online DDL 的三个阶段

MySQL 在线 DDL 过程:
  阶段1:准备(拿 MDL 写锁,短)
  阶段2:执行(In-place 重建 / Copy)
  阶段3:提交(拿 MDL 写锁,短)

并发控制由参数决定:
  ALGORITHM  = INSTANT | INPLACE | COPY
  LOCK       = NONE | SHARED | EXCLUSIVE
  → ALGORITHM 决定"怎么改",LOCK 决定"允许的并发度"

2.2 ALGORITHM 与 LOCK 决策

-- 尽量选 NONE:变更期间允许并发读写
ALTER TABLE orders
  ADD COLUMN status TINYINT DEFAULT 0,
  ALGORITHM=INPLACE, LOCK=NONE;

-- 明确拒绝阻塞:若不能 NONE 就报错(安全策略)
ALTER TABLE orders
  ADD INDEX idx_user (user_id),
  ALGORITHM=INPLACE, LOCK=NONE;

-- 只改元数据(最快)
ALTER TABLE orders
  ALTER COLUMN note SET DEFAULT 'x',
  ALGORITHM=INSTANT;

2.3 各类操作的算法与锁

操作算法允许并发说明
加列(末尾,无前插)INSTANT读写MySQL 8.0.12+
加普通二级索引INPLACE读写建索引期间可 DML
加全文索引INPLACE读写
加/删列(非末尾)INPLACE读写
改默认值INSTANT读写
改主键COPY只读(按 LOCK)重建表
改字符集COPY只读(按 LOCK)重建表
压缩表COPY只读
加外键INPLACE读写

⚠️ 常见误区:加普通索引能 INPLACE 且并发读写,但"执行阶段长"——大表建索引同样耗资源。真正要防的是 COPY 类操作 + LOCK=EXCLUSIVE。

2.4 原理解读:加索引为什么能并发写

InnoDB 加二级索引的 INPLACE 过程:
  1. 读现有聚集索引,生成新索引的 B+ 树(后台线程)
  2. 期间 DML 正常执行,同时把变更记录到 redo log
  3. 提交时用 redo 把增量合并进新索引

→ 设计成"构建索引"与"业务写"并行,各自记录、最终合并
→ 关键风险:redo 增长、磁盘 IO 压力、长事务

三、PostgreSQL 的 DDL 特点

3.1 PG 的 MVCC 架构天然优势

PostgreSQL 每个表 = 堆文件 + 索引
ALTER 大部分操作是"加列/加索引":

加列:
  ALTER TABLE ADD COLUMN → 只更新 catalog 元数据
  → O(1) 完成,不重写表(版本 12+ 不再需要整表重写,仅加物理空列)

加索引:
  CREATE INDEX CONCURRENTLY → 与业务并发建索引
  三阶段:扫描建树 → 等待 → 增量合并
  → 不阻塞 DML,只需短锁

修改类型/删除列:需要重写表
  → 使用 PG 12+ 的"默认并行/触发重写",仍需谨慎

3.2 CONCURRENTLY 建索引

-- 并发建索引:不阻塞读写
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);

-- 大表场景:分块执行,避免一次性资源占用
SET maintenance_work_mem = '1GB';   -- 建索引内存
SET max_parallel_maintenance_workers = 8;   -- 并行度

3.3 PG 变更限制与对策

操作是否重写表并发注意
加列(默认值常量)12+ 不重写不阻塞快速
加列(有 volatile 默认值)重写阻塞避免
加索引不重写CONCURRENTLY推荐
改列类型重写阻塞分批/工具
删列重写(DROP)阻塞大表谨慎
约束(CHECK/NOT NULL)部分重写分阶段用 ADD CONSTRAINT … NOT VALID
-- 大表加 NOT NULL 约束的安全三步(PG 12+)
ALTER TABLE orders ADD CONSTRAINT c_required CHECK (user_id IS NOT NULL) NOT VALID;
-- 校验现有数据(慢,但不阻塞)
ALTER TABLE orders VALIDATE CONSTRAINT c_required;

四、第三方工具:gh-ost 与 pt-osc

4.1 为什么还需要第三方工具

数据库原生在线 DDL 的局限:
  · MySQL 部分操作仍 COPY + 需手动权衡锁
  · 无法控制"变更期间的复制延迟"(主从)
  · 变更大表时 redo/binlog 增量可能失控

第三方工具的思路(trigger-less 更好):
  · 创建新表(改好的结构)
  · 从旧表拷贝存量数据到新表(后台、限速)
  · 拷贝期间持续同步增量(binlog / 触发器)
  · 校验一致 → 原子换名(RENAME)
  → 全程不阻塞写,主从可逐步切换

4.2 gh-ost(GitHub,trigger-less)

# gh-ost 核心优点:基于 binlog 同步,无触发器开销
gh-ost \
  --host=db-1 --port=3306 \
  --user=app --password=*** \
  --database=orders_db \
  --table=orders \
  --alter="ADD COLUMN status TINYINT DEFAULT 0" \
  --allow-on-master \
  --execute

# 常用控制参数
--max-load=Threads_running=200          # 负载过高自动减速
--max-lag-millis=1500                   # 从库延迟上限
--chunk-size=1000                       # 拷贝批大小
--throttle-control-replicas="db-2"      # 按从库延迟限速
--cut-over-lock-timeout-seconds=1       # 切换锁超时
--execute                               # 真正执行(否则 dry-run)

4.3 pt-osc(Percona,基于触发器)

# pt-osc 使用触发器跟踪增量,与 gh-ost 思路不同
pt-online-schema-change \
  --alter "ADD COLUMN status TINYINT DEFAULT 0" \
  --chunk-size=1000 \
  --max-lag=1 \
  --max-load "Threads_Running=200" \
  --critical-load "Threads_Running=500" \
  D=orders_db,t=orders

4.4 gh-ost vs pt-osc

维度gh-ostpt-osc
增量同步binlog(无触发器)触发器(INSERT/UPDATE/DELETE 三 trigger)
对源库影响低中(触发器增加开销)
切换原子 cut-over原子换名
从库一致需监听从库延迟需监听从库延迟
适用8.0 / 触发器敏感老版本 / binlog 不可用

ℹ️ 生产建议:MySQL 8.0 首选 gh-ost(无触发器副作用);变更时务必 --execute 前先 dry-run,并设 --max-lag 与 --max-load 兜底。


五、变更安全策略与最佳实践

5.1 大表 DDL 标准流程

1. 评估操作类型(INSTANT? INPLACE? COPY?)
2. 估算成本(表大小、rows、磁盘空间、redo)
3. 低峰期执行 + 窗口审批
4. 先小表演练,再大表
5. 全程监控:锁等待、redo 增长、IO、从库延迟
6. 预留回滚预案(备份/快照/可回滚设计)
7. 变更后验证:数据一致性 + 性能

5.2 避免踩坑的硬规则

· 禁止在主库直接 COPY 级 ALTER 大表(用 gh-ost/pt-osc)
· 变更前确认磁盘空间(重建表需≈表大小空间)
· 观察长事务:长事务持有 MDL,会阻塞 DDL
· 主从场景:先备库变更 → 灰度切换 → 再主库变更
· 设置 MDL 锁超时(lock_wait_timeout)防无限等待
· 监控 performance_schema.metadata_locks 定位持锁者

5.3 应急预案

-- 定位阻塞 DDL 的会话
SELECT * FROM performance_schema.metadata_locks
WHERE LOCK_STATUS='PENDING';

-- 定位持锁长事务并评估终止
SELECT * FROM information_schema.innodb_trx
WHERE trx_state != 'COMMITTED' ORDER BY trx_started;

-- 极端情况:终止阻塞会话(谨慎)
CALL mysql.rds_kill(?);   -- 或 KILL <thread_id>

六、增量数据同步的工程实现

6.1 gh-ost 的同步模型

gh-ost 三阶段:
  1. 建新表(目标结构)
  2. 拷贝存量:SELECT 旧表 → chunk 写入新表
     同时应用 binlog 增量(INSERT/UPDATE/DELETE)
  3. cut-over:RENAME old→old_hidden, new→old(原子)

增量同步关键:
  · binlog 事件按主键应用,顺序一致
  · 拷贝期间主键并发写 → 用唯一键去重
  · 校验:chunk 数 / 行数对比

6.2 校验与一致性

变更后一致性验证:
  · 行数对比(COUNT 或校验和)
  · 抽样对比字段值
  · 索引完整性检查(ANALYZE / 查询性能测试)
  · 主从:对比主备库表结构一致

七、MySQL 8.0 INSTANT 的新能力

7.1 即时加列

-- 8.0.12+:末尾加列即时完成
ALTER TABLE orders ADD COLUMN remark VARCHAR(100), ALGORITHM=INSTANT;

-- 约束:不能加在非末尾 / 不能加主键 / 瞬时加列数有限制
-- 每表 INSTANT 列数量受 row 大小与版本限制

7.2 在线修改的其他新能力

-- 修改列默认值:INSTANT
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 1;

-- 索引改可见性/隐藏:INPLACE
ALTER TABLE orders ALTER INDEX idx_user INVISIBLE;

⚠️ 陷阱:ALGORITHM=INSTANT 失败会退回 INPLACE/COPY 并阻塞——生产环境应显式指定算法并在预演库验证,或依赖 gh-ost 兜底。


八、分布式数据库的 DDL(TiDB 等)

8.1 TiDB 的在线 DDL

TiDB 的 DDL 设计(借鉴 Google F1):
  · 异步 DDL:变更请求提交后由 DDL Owner 后台执行
  · 多阶段状态机:none → delete-only → write-only → ready → public
  · 全程不阻塞读写
  · 可随时暂停/恢复(ADMIN PAUSE DDL)

ALTER TABLE orders ADD COLUMN status TINYINT DEFAULT 0;  -- 立即返回,后台执行

8.2 分布式 DDL 的取舍

· 优点:大表无感、可并行、可取消
· 代价:实现复杂、元数据一致性依赖一致协议
· 云原生库(PolarDB/Aurora)多数也提供"异步变更"

九、工具链与落地清单

9.1 完整决策树

变更类型判断:
  MySQL:
    INSTANT 可做 → 直接 ALTER(毫秒)
    INPLACE 可做 → 可 ALTER + LOCK=NONE(注意执行时长)
    COPY 才可做 → gh-ost / pt-osc(推荐)
  PostgreSQL:
    加列/加索引 → 原生(CONCURRENTLY 建索引)
    重写类 → 评估分批 / 工具 / 停机窗口

9.2 落地 Checklist

□ 识别变更算法与锁模型
□ 磁盘/内存/IO 容量预评估
□ 低峰窗口 + 变更审批
□ dry-run 演练(gh-ost --test-on-replica 更佳)
□ 监控:锁等待、redo、IO、从库延迟
□ max-load / max-lag 兜底限速
□ 回滚预案(备份、快照)
□ 变更后一致性 + 性能验证

9.3 常见错误速查

错误后果对策
直接 ALTER 亿级表锁阻塞数小时gh-ost/pt-osc
长事务持 MDLDDL 一直 pending先杀长事务
磁盘不足做 COPY写满崩溃预留空间
主库大表 DDL从库复制延迟备库先行
不设 max-lag从库崩溃必设限速

总结:在线 DDL 核心决策表

环节关键动作
评估判断 INSTANT/INPLACE/COPY + 锁模型
原生能力MySQL Online DDL + PG CONCURRENTLY
大表兜底gh-ost(binlog 同步)/ pt-osc(触发器)
安全低峰 + 审批 + 限速 + 监控 + 回滚
一致性变更后行数/字段/主从校验

在线 DDL 的本质是一场"与业务并发赛跑的换轮胎"——要么让数据库原生支持原地修改(INSTANT/INPLACE),要么用 gh-ost 这类工具在"copy 存量 + 追增量 + 原子切换"中完成结构升级,全程不锁写。落地时守住三件事:先判算法(别让 COPY 悄悄堵库)、大表必用工具(gh-ost 带限速兜底)、全程可回滚(备份 + 监控 + 主从先行)。把在线 DDL 当成一次有计划的工程变更而非裸 ALTER,大表结构升级就从"运维事故"变成"例行操作"。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 数据库安全加固与审计实战:权限最小化、加密、脱敏与合规
  2. 数据库容量规划与资源治理:从评估、监控到扩展路径
  3. 数据库字符集、排序规则与乱码实战:utf8mb4、Collation 选择与排查