4. 修改列类型对存储的影响:VARCHAR长度扩展与收缩的成本、INT到BIGINT的存储膨胀、DECIMAL精度变化带来的空间变化

列类型变更,是日常 Schema 变更里最容易被低估成本的操作之一。很多人觉得「我就改个字段长度,能花多少钱?」——嗯,我见过因为把 VARCHAR(50) 改成 VARCHAR(5000) 导致单表空间暴涨 3 倍的案例。今天咱们就把 VARCHAR、INT、DECIMAL 这三种常见类型的变更成本掰开揉碎讲清楚。

4.1 VARCHAR 长度扩展:你以为只是改个定义?

VARCHAR 是变长字段,存储时只占用实际字符长度 + 1~2 字节的长度前缀。但这里有个关键点:MySQL 的 VARCHAR 长度限制与行格式、字符集、索引前缀深度绑定。

核心结论:VARCHAR 从短变长,如果新长度 ≤ 255,存储空间不变;如果新长度 > 255,长度前缀从 1 字节变成 2 字节,每行多占 1 字节。但真正的成本在于——Online DDL 期间可能触发重建表。

我个人的习惯是:VARCHAR 长度一旦超过 255,就要重新评估索引和行格式。举个例子:

-- 原表定义
CREATE TABLE user_profile (
    bio VARCHAR(200) CHARACTER SET utf8mb4
) ENGINE=InnoDB;

-- 变更为
ALTER TABLE user_profile MODIFY bio VARCHAR(500) CHARACTER SET utf8mb4;

这个变更看起来人畜无害,对吧?但实际上:

  • 长度从 200 → 500,跨越了 255 阈值,长度前缀从 1 字节变成 2 字节
  • 如果该字段上有索引,InnoDB 需要重建索引(索引前缀长度可能变化)
  • utf8mb4 字符集下,最大字节长度从 800 变成 2000,可能触发行溢出

我在项目中遇到过这样一个坑:某社交应用把用户签名从 VARCHAR(100) 改成 VARCHAR(1000),结果因为行溢出导致 溢出页频繁读取,IO 延迟从 2ms 飙升到 50ms。你以为只是存储成本增加?性能成本才是大头。

避坑指南:我曾经在线上直接执行 MODIFY COLUMN 改 VARCHAR 长度,结果触发了全表重建,500GB 的表跑了 6 小时。后来我学乖了——先检查是否真的需要重建:如果只是长度扩展且不超过 255,可以用 ALGORITHM=INPLACE;如果超过 255 或涉及字符集变更,务必在低峰期操作。

4.2 VARCHAR 长度收缩:空间不会自动归还

很多人以为把 VARCHAR(500) 改成 VARCHAR(50),存储空间会自动释放。事实是——InnoDB 不会因为长度收缩而回收已分配的空间。

为什么?因为 InnoDB 的页分配是以 区(extent) 为单位的,收缩字段长度只是修改了数据字典中的字段定义,已经写入磁盘的数据页不会自动合并或释放。你需要执行 OPTIMIZE TABLE 或重建表才能真正回收空间。

操作 存储空间变化 是否需要重建表 建议
VARCHAR 扩展(≤255) 不变 否(INPLACE) 可在线执行
VARCHAR 扩展(>255) 每行+1字节 是(可能触发) 低峰期操作
VARCHAR 收缩 不变(需重建) 是 评估收益后再做

警告:VARCHAR 收缩时,如果新长度小于现有数据的最大长度,会导致 数据截断。我曾经见过有人把 VARCHAR(200) 改成 VARCHAR(50),结果 30% 的用户昵称被截断,线上事故。收缩前务必先检查数据分布:SELECT MAX(LENGTH(column_name)) FROM table_name;

4.3 INT 到 BIGINT:存储膨胀 100%

INT 占 4 字节,BIGINT 占 8 字节。从 INT 改成 BIGINT,存储空间直接翻倍。但你以为只是字段本身翻倍?

实际上,InnoDB 的二级索引会复制主键值。如果这个字段是主键,那么:

  • 主键索引:每行多 4 字节
  • 每个二级索引:每行也多 4 字节(因为二级索引叶子节点包含主键值)
  • 如果有 5 个二级索引,总膨胀量 = 4 × (1 + 5) = 24 字节/行

我算过一笔账:一张 1 亿行的表,主键从 INT 改成 BIGINT,如果有 3 个二级索引,总存储膨胀约 400MB。看起来不多?但别忘了,缓冲池(Buffer Pool)也会跟着膨胀,内存成本同样翻倍。

个人经验:我建议在设计阶段就预估数据量。如果预计行数超过 20 亿(INT 上限约 42 亿,但实际业务往往留余量),直接上 BIGINT。中途改类型,不仅存储成本翻倍,DDL 锁表时间也长。我见过一个电商系统,订单表从 INT 改成 BIGINT,花了 8 小时,期间业务只读。

4.4 DECIMAL 精度变化:精度越高,空间越贵

DECIMAL 的存储空间计算规则是:每 9 位十进制数字占 4 字节,剩余部分按 4、3、2、1 字节分配。具体来说:

DECIMAL(M, D) 整数部分位数 小数部分位数 存储字节数
DECIMAL(10, 2) 8 2 5
DECIMAL(18, 2) 16 2 8
DECIMAL(30, 10) 20 10 13

从 DECIMAL(10,2) 改成 DECIMAL(18,2),存储从 5 字节变成 8 字节,膨胀 60%。如果改成 DECIMAL(30,10),直接变成 13 字节,膨胀 160%。

我在金融项目中遇到过:某对账系统把金额字段从 DECIMAL(12,2) 改成 DECIMAL(20,8) 以支持数字货币精度,结果存储成本增加了 2.3 倍。更麻烦的是,DECIMAL 精度变更通常需要重建表,因为数据需要重新编码。

建议:DECIMAL 精度一旦确定,尽量不改。如果确实需要更高精度,可以考虑用 BIGINT 存储最小单位(比如分、厘),这样既避免 DECIMAL 的计算开销,也避免精度变更带来的存储膨胀。我个人的经验是:99% 的业务场景,BIGINT 存最小单位比 DECIMAL 更省空间、更快。

4.5 知识体系:列类型变更的存储影响全景

下面这张图总结了本章的核心逻辑,方便你快速回顾:

列类型变更对存储的影响全景 列类型变更 VARCHAR 长度变更 INT ↔ BIGINT DECIMAL 精度变更 扩展 ≤255:空间不变 扩展 >255:+1字节/行 收缩:需重建表回收 存储翻倍(4→8字节) 二级索引同步膨胀 Buffer Pool 成本翻倍 每9位占4字节 精度越高膨胀越大 通常需要重建表 核心原则:变更前评估存储膨胀 + 重建成本 设计阶段预留空间,避免中途改类型

4.6 总结:变更前必做的三件事

  1. 评估存储膨胀系数:计算每行增加的字节数,乘以行数,再乘以索引数量。别忘了 Buffer Pool 也会跟着涨。
  2. 检查是否需要重建表:VARCHAR 收缩、DECIMAL 精度变更、跨越 255 阈值的扩展,基本都需要重建。重建期间的空间放大(临时表)也要算进成本。
  3. 考虑替代方案:能用 BIGINT 存最小单位就别用 DECIMAL;能用 VARCHAR(255) 就别用 VARCHAR(1000)。设计阶段的「偷懒」,往往变成运维阶段的「还债」。

最后提醒:列类型变更的存储成本,往往不是单字段的线性增长,而是 索引、行格式、缓冲池 的连锁反应。我建议每次变更前,先用 INFORMATION_SCHEMA.TABLES 和 INFORMATION_SCHEMA.COLUMNS 做一次数据分布分析,别凭感觉改。