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 知识体系:列类型变更的存储影响全景
下面这张图总结了本章的核心逻辑,方便你快速回顾:
4.6 总结:变更前必做的三件事
- 评估存储膨胀系数:计算每行增加的字节数,乘以行数,再乘以索引数量。别忘了 Buffer Pool 也会跟着涨。
- 检查是否需要重建表:VARCHAR 收缩、DECIMAL 精度变更、跨越 255 阈值的扩展,基本都需要重建。重建期间的空间放大(临时表)也要算进成本。
- 考虑替代方案:能用 BIGINT 存最小单位就别用 DECIMAL;能用 VARCHAR(255) 就别用 VARCHAR(1000)。设计阶段的「偷懒」,往往变成运维阶段的「还债」。
最后提醒:列类型变更的存储成本,往往不是单字段的线性增长,而是 索引、行格式、缓冲池 的连锁反应。我建议每次变更前,先用 INFORMATION_SCHEMA.TABLES 和 INFORMATION_SCHEMA.COLUMNS 做一次数据分布分析,别凭感觉改。