数据库大表DDL变更实战指南

详解数据库大表DDL变更的各种方案、优化策略及注意事项,助你提升数据库性能。

原文标题:数据库中大表DDL变更问题及优化

原文作者:牧羊人的方向

冷月清谈:

本文探讨了数据库中大表DDL变更的常见问题及优化策略。首先,文章介绍了MySQL数据库中三种DDL实现算法:COPY、Inplace和Instant,并分析了它们各自的优缺点和适用场景。COPY算法通过创建临时表复制数据,执行速度慢且会阻塞DML操作;Inplace算法直接修改原表,效率更高,但部分操作仍需重建表;Instant算法仅修改元数据,速度最快,但适用范围有限。
文章还特别提到了VARCHAR字段扩长的问题。由于VARCHAR字段存储需要长度前缀,当扩长导致长度前缀字节数增加时,MySQL会使用COPY算法重建表,耗时较长。而其他数据库如Oracle、PostgreSQL和GaussDB则不存在这个问题。
最后,文章总结了大表DDL变更的优化方案,包括:将修改字段类型操作转换为新增字段,通过后台迁移数据减少业务影响;将大表重建优化为数据迁移,分阶段进行以缩短变更窗口;评估大表的DDL变更时长,选择合适的变更方案;手动更新统计信息,避免SQL执行计划错误;注意DDL操作对主备同步的影响,避免数据延迟。

怜星夜思:

1、除了文中提到的方案,还有哪些方法可以优化大表DDL变更?例如,有没有一些工具或技术可以进一步减少停机时间?
2、文章提到了VARCHAR字段扩长的问题,那么在实际应用中,如何避免这个问题呢?在设计数据库表结构时,应该如何选择VARCHAR的长度?
3、文章主要以MySQL为例,那么其他类型的数据库,例如Oracle、PostgreSQL等,在大表DDL变更方面有哪些最佳实践?

原文内容

应用系统中数据库的DDL变更操作是一个非常常见的场景,但是对于一些DDL变更的原理以及变更影响评估在理解上似乎有些不足。本文梳理DDL实现算法、Varchar字段扩长影响以及大表DDL变更的优化方案等,以加深理解。

1、数据库中的DDL变更

随着业务的发展,应用上线以后需要对数据库表进行添加字段、修改字段类型或者默认值等DDL变更操作。不同数据库在不同的DDL执行的时候有在线和非在线等类型,在线DDL指的是DDL执行过程中不影响业务的读写操作,非在线DDL则会堵塞业务的写操作。因此在数据库的DDL变更中,尽可能减少非在线DDL操作,或者将非在线转换为在线DDL、缩短业务影响窗口。本文主要以MySQL为例介绍数据库中DDL实现的算法。

1.1 MySQL数据库DDL实现算法

MySQL数据库中DDL实现算法主要有Copy、Inplace和Instant三种,COPY算法是MySQL 5.5以及之前版本的默认算法,随着MySQL版本的迭代,之后的版本引入了Inplace和Instant算法,以减少DDL变更期间的业务影响。不过不是所有的DDL操作都能在线完成或者秒级完成,尤其是一些字段类型的DDL变更,而是默认情况下由数据库自己去选择哪种最优的算法实现。

1.1.1 COPY算法

COPY算法的基本原理是通过Server层创建临时表,将原表数据逐行复制到新表结构,最终替换原表。该过程会阻塞读以外的DML操作,且需要大量I/O和磁盘空间。因此使用COPY算法会锁表,属于非在线DDL。COPY算法执行流程如下:

  • 准备阶段:
    • 对表加DML元数据共享锁,读取元数据
    • 升级DML元数据为排他锁,禁止其它写操作
    • 按照原表定义创建一个新的临时表,生成对应ibd,frm文件
  • 执行阶段
    • 对临时表进行DDL,修改临时表元数据
    • 将原表中的数据copy到临时表(最耗时)
    • 将临时表重命名为原表
    • 删除原表和文件
  • 提交阶段:
    • 释放原表的写锁
1.1.2 Inplace算法


与COPY算法不同,INPLACE算法直接在原始表上进行修改,无需创建临时表和拷贝数据。另外copy是在Server层处理的,INPLACE是在innodb引擎层处理的。分为两种模式:

  • No-Rebuild:仅修改元数据(如添加二级索引),无需重建表,速度极快。
  • Rebuild:需重建表(如修改主键、增减列),期间生成临时排序文件,允许DML并发,但需记录变更日志(row log)并最终应用

Rebuild是在线完成的,只是对元数据锁的操作会短暂锁表,对业务基本无感。主要包括3个阶段:prepare阶段,ddl执行阶段,commit阶段,rebuild方式比no-rebuild方式实质多了一个ddl执行阶段,prepare阶段和commit阶段类似。

  • Prepare阶段:
    • 创建新的临时frm文件
    • 持有EXCLUSIVE-MDL锁,禁止读写
    • 根据alter类型,确定执行方式(copy,online-rebuild,online-norebuild)
    • 更新数据字典的内存对象
    • 分配row_log对象记录增量(仅rebuild类型需要)
    • 生成新的临时ibd文件(仅rebuild类型需要)
  • ddl执行阶段:
    • 降级EXCLUSIVE-MDL锁,允许读写
    • 扫描old_table的聚集索引每一条记录rec
    • 遍历新表的聚集索引和二级索引,逐一处理
    • 根据rec构造对应的索引项
    • 将构造索引项插入sort_buffer块
    • 将sort_buffer块插入新的索引
    • 处理ddl执行过程中产生的增量(仅rebuild类型需要)
    • 重放row_log中的操作到新索引上(no-rebuild数据是在原表上更新的)
    • 重放row_log间产生dml操作append到row_log最后一个Block
  • commit阶段
    • 升级到EXCLUSIVE-MDL锁,禁止读写
    • 重做row_log中最后一部分增量
    • 更新innodb的数据字典表
    • 提交事务(刷事务的redo日志)
    • 修改统计信息
    • rename临时idb文件,frm文件
1.1.3 Instant算法

Instant算法仅修改数据字典中的元数据(如添加列到表末尾),无需拷贝数据或重建表,执行时间极短(毫秒级)。Instant算法是在MySQL 8.0.12及以上版本支持,适用于添加列、修改列默认值、重命名列等有限操作。

其它数据库中DDL的操作和MySQL类似,也分为在线DDL和非在线DDL,DDL的实现算法上也大致一致。比如OceanBase数据库的非在线DDL使用的是两表双写的方案。两表双写方案的思路是新建一张临时的隐藏表格(对用户不可见)用于双写,同时在后台将原表的数据补全到新建的临时表格中,然后将原表重命名为一个另外的临时表格,将补全数据的临时表格重命名为原表原来的名字,最后将原表的删除。在实现原理上实现其实跟MySQL的COPY方式实现原理非常的像。

1.2 不同DDL操作实现情况

MySQL 8.0以后版本在执行DDL时候使用DEFAULT,也就是由数据库自动选择资源占用最少得方式,优先选择Instant和Inplace,无法满足时使用COPY算法。MySQL默认对不同的DDL使用的算法如下:

比如修改字段类型,无法使用Instant和Inplace算法,只能使用COPY算法实现,这是个非在线DDL实现过程。

2、Varchar字段扩长的问题

在MySQL中VARCHAR 字段的存储需要 长度前缀(Length Prefix) 来记录实际数据的字节数。长度前缀占用的字节数由字段的最大允许长度决定

  • 1字节:当VARCHAR字段的最大可能字节数≤ 255时(例如VARCHAR(63)使用utf8mb4字符集,每个字符最多占4字节,总字节数为63 × 4 = 252)。
  • 2字节:当最大可能字节数>255 时(例如 VARCHAR(64) 使用 utf8mb4,总字节数为 64 × 4 = 256)。

由于数据库默认的字符集是utf8mb4,每个字符最多占4个字节:ASCII字符(英文字母、数字等)占1字节、希腊字母等占2个字节、常用汉字等占3个字节、一些表情符和生僻汉字等占4个字节。Varchar字段的临界长度=255/字符集最大字节数,对于字符集为utf8mb4,临界值为255 / 4 = 63.75,即63字符是临界点。所以当Varchar字段扩长时,有两种情况:

  • 若修改字段长度后,长度前缀的字节数未变化(例如从50改到63),仅需更新元数据,无需重建表数据。
  • 若修改字段长度后,长度前缀的字节数增加(例如从 63 改到 64,导致长度前缀从1字节变为2字节),则需使用上文中的COPY算法重建表数据,导致DDL操作耗时变长。

因此,基于MySQL数据库内核二次开发的信创数据库如GoldenDB、TDSQL(for MySQL)、GreatSQL等,如果这一块在内核层面没有优化,还是存在这部分问题的。其它类型的数据库如Oracle、PostgreSQL和GaussDB等,在varchar扩长时不会出现存储格式的变化,只需要更新元数据,秒级完成。

3、数据库大表DDL变更优化

目前大多数据库在字段类型修改等场景还不支持在线DDL,即使是在线DDL操作,如果涉及到大表重建,整个DDL变更窗口也是相当长的。那么在DDL变更过程中,有哪些优化点或注意地方?

1)修改字段类型变成新增字段

直接在表中新增字段,有两个方向:一是后续的业务直接在这个新增的字段中操作,从开发的角度来说无疑增加了程序处理的难度,旧有的字段数据访问需要程序逻辑加以控制;另一种是新建临时字段后在后台将原有字段数据迁移到新字段,并且在过渡阶段应用同时更新新旧字段,等旧字段数据迁移完成后,在停机窗口进行新旧字段的切换。无论哪一种,都增加了应用处理上的复杂度。

2)大表重建优化为数据迁移的过程

这是大表在非在线DDL或者DDL耗时过长时候的一个优化方向,基本思路是在T-n日的窗口新建临时表并进行存量数据迁移,等到正式投产时候增量迁移当天的数据,并完成新旧表切换。这种方案可以避免大表DDL变更带来的变更窗口问题,风险点在于实现一整套数据迁移的流程,尤其是要完成数据的校验和核对,不能出现变更前后数据不一致的情况。

3)评估大表的DDL变更时长

确认了DDL变更的类型后,根据不同数据库能够明确是在线还是非在线,是否有表重建或build索引的操作,然后根据表大小来评估DDL变更的时长。通常在使用COPY算法重建表数据时,效率为1~1.5G/1min,表重建完成后还要基于表来build索引,效率约为0.5G/1min,而索引越多,build索引阶段耗时越长

变更类型
在线
重建表
数据页
索引页
评估耗时
新增字段
100G
20G
<1s
字段类型修改
100G
20G
140min

所以在评估大表DDL变更时长,最好在演练环境先进行性能测试以做到时间预期,这个评估时长也决定了应用DDL变更的业务影响窗口,如果窗口有明确限制,比如不能超过2小时,则需要考虑其他方案,比如新建表在线迁移的方案。从这里也看到分布式数据库的其中一个优点,就是实现分库分表的操作,单个库中表尽量小,这样在DDL操作的时候利用分布式的并发特性,也可以缩短整体的变更时长。

4)统计信息的更新

在MySQL数据库中当DDL使用COPY算法时候,可能会遇到分区表统计信息丢失的bug(Bug #113750),如果在做完DDL时候没有注意到这一点,也可能会出现统计信息不准确,进而影响到SQL的执行计划导致业务处理效率变慢。MySQL系的数据库中如果没有专门针对这个问题优化,都会有潜在的影响的。

当大表在做完DDL后,尤其是涉及到表重建的操作,遇到统计信息可能发生了变化,最安全的操作是手动执行一个统计信息收集的操作。比如MySQL系数据库中的Analyze table,这个也是秒级完成的。

5)DDL操作过程的影响

DDL操作像非在线DDL会锁表影响写操作、涉及到表重建的DDL会有大IO操作,因此尽量在业务低峰期执行DDL、将非在线DDL转换为在线DDL(在线DDL工具pt-osc/gh-ost等)、大表的DDL使用分阶段数据迁移方案等。同时需要注意的是DDL操作是一个事务的原子性操作,当DDL事务未执行完成,备机是无法同步这个事务,因此在DDL执行过程中会出现备机同步阻塞导致主备之间出现数据延迟。对于一些有需要使用到备机抽数的场景,错开DDL执行的时间窗口,否则会出现数据不同步的情形。

参考资料:

  1. https://blog.csdn.net/OceanBaseGFBK/article/details/138126035
  2. https://opensource.actionsky.com/20190517-mysql-ddl/
  3. https://bugs.mysql.com/bug.php?id=113750
  4. InnoDB在线DDL实现机制
  5. https://zhuanlan.zhihu.com/p/567083306

其他数据库的最佳实践,其实也离不开文章提到的那些核心思想,例如最小化停机时间、减少对业务的影响等等。具体的实现方式可能有所不同,但总的来说,都是为了提高DDL效率,降低风险。

我来抖个机灵:最优的方案当然是祈祷数据库自己进化出更牛的DDL算法,秒级完成,无痛升级!
说正经的,除了文中提到的和楼上说的,还可以关注一下数据库的新版本特性,有些新版本会针对DDL性能进行优化,升级版本或许也能解决问题。另外,有些云数据库厂商也提供了一些在线DDL服务,可以了解一下。

对于“文章主要以MySQL为例,那么其他类型的数据库,例如Oracle、PostgreSQL等,在大表DDL变更方面有哪些最佳实践?” 这个问题,我可以分享一些 Oracle 的经验。
Oracle 提供了在线重定义表的功能,可以实现在不停机的情况下进行DDL操作。此外,Oracle 还支持分区表和子分区表,可以将大表分成多个小表,从而提高DDL效率。当然,在使用这些功能时,也需要注意一些细节,例如分区键的选择、数据的分布等等。

对于这个问题,我想说一句:如果实在拿不准,就大胆地上 TEXT 类型吧! (逃
开个玩笑,TEXT 类型虽然解决了长度问题,但也有其自身的局限性,例如不能建立索引。所以在实际应用中,还是要根据具体情况选择合适的字段类型。

就 PostgreSQL 而言,它也提供了一些在线DDL工具,例如 pg_repack。此外,PostgreSQL 还支持逻辑复制功能,可以将数据复制到新的表结构中,然后进行切换。选择哪种方案取决于具体的业务需求和表的大小。

关于“文章提到了VARCHAR字段扩长的问题,那么在实际应用中,如何避免这个问题呢?在设计数据库表结构时,应该如何选择VARCHAR的长度?”这个问题,我有一些想法。
避免这个问题的核心在于预估好字段长度,并留有一定的余量。在设计表结构的时候,需要充分考虑未来业务发展的可能性,预估字段可能存储的最大长度。
当然,也不能一味求大, VARCHAR 字段过长也会导致存储空间的浪费和查询效率的降低。

关于这个问题,我觉得可以考虑一下利用数据库的并行处理能力。比如,有些数据库支持并行DDL,可以将DDL操作分解成多个子任务并行执行,从而加快DDL的速度。另外,还可以考虑在备库上执行DDL,然后主备切换,这样也可以减少对主库的影响。当然,具体的方案需要根据具体的数据库和业务场景来选择。