3. 新增列对存储的影响:NULLABLE 与 NOT NULL 的成本差异
这一节,我们来聊聊新增列时最容易踩的坑。说白了,就是加一列到底要花多少钱。
很多人觉得加列不就是一条 ALTER TABLE 嘛,几秒钟的事。嗯,如果你这么想,那账单可能会让你吓一跳。我见过不少团队,因为加列姿势不对,导致存储成本翻倍,甚至引发生产故障。
3.1 NULLABLE 列 vs NOT NULL 列:存储成本差异
先看一个最基础的问题:新加的列允许为空,还是不允许为空?
在大多数数据库引擎里,NULLABLE 列需要额外的标记位来记录“是否为空”。比如在 MySQL InnoDB 中,每行会有一个 NULL 位图,每个可为空的列占用 1 bit。虽然单行看起来不多,但几亿行累积下来,就是几百 MB 甚至 GB 级的开销。
我举个例子:
-- 场景 A:新增 NULLABLE 列
ALTER TABLE orders ADD COLUMN discount_rate DECIMAL(5,2) NULL;
-- 场景 B:新增 NOT NULL 列(带默认值)
ALTER TABLE orders ADD COLUMN discount_rate DECIMAL(5,2) NOT NULL DEFAULT 0.00;
这两条语句,存储成本差多少?
| 对比维度 | NULLABLE 列 | NOT NULL 列(带默认值) |
|---|---|---|
| 每行额外开销 | 1 bit(NULL 位图)+ 实际数据长度 | 仅实际数据长度 |
| 默认值存储 | NULL 不占数据空间(位图标记) | 默认值写入每行数据区 |
| 1 亿行额外空间 | 约 12 MB(位图)+ 数据 | 约 0 MB(位图)+ 数据 |
| 查询性能影响 | IS NULL 判断增加 CPU 开销 | 无额外判断 |
关键结论:如果新列 99% 的场景都有值,就别偷懒设成 NULLABLE。NOT NULL + 默认值,既省空间又省 CPU。
我个人习惯是:能用 NOT NULL 就别用 NULLABLE。除非这个列确实有“未知”或“不适用”的语义,否则 NULL 只会带来麻烦。
3.2 默认值对存储空间的影响
这里有个容易混淆的点:默认值到底会不会占用存储空间?
答案是:看数据库实现。
- MySQL InnoDB:新增 NOT NULL 列时,默认值会立即写入每一行。这意味着 ALTER TABLE 会重写整个表,产生大量 IO 和临时空间。
- PostgreSQL:从某个版本开始,新增 NOT NULL 列带常量默认值时,只改元数据,不重写行。存储空间几乎不增加。
- SQL Server:类似 PostgreSQL,有“元数据优化”路径,但有限制条件。
我在项目中遇到过这样一个案例:
某团队在 MySQL 5.7 上给一张 5 亿行的表加列,用了 NOT NULL DEFAULT 'pending'。结果 ALTER 跑了 6 个小时,临时表空间撑爆了磁盘,直接导致业务中断。后来我帮他们改成先加 NULLABLE 列,再分批 UPDATE 填充数据,最后加 NOT NULL 约束。虽然步骤多了,但磁盘压力可控,业务零影响。
避坑指南:在 MySQL 中,新增 NOT NULL 列带默认值,会触发全表重建。如果你的表超过 1000 万行,建议用 pt-online-schema-change 或 gh-ost 工具,或者分步操作。
3.3 新增列后的数据页分裂问题
这一节是很多人忽略的。加列不只是“加一列”那么简单,它可能引发数据页分裂。
为什么会这样?
数据库的数据页大小是固定的(MySQL 默认 16KB)。当新增列导致行长度增加,原本能塞下 N 行的页,现在只能塞下 N-1 行。如果表的主键是自增 ID,新插入的行会不断往页里填,直到页满。页满后,数据库必须分裂出新的页,这个过程会:
- 产生大量碎片
- 增加索引层级
- 降低查询性能
- 放大存储空间(碎片不回收)
我画了一张图,帮你理解这个过程:
你看,分裂前一个页能装 8 行,分裂后两个页加起来还是装 8 行,但占用的磁盘空间翻倍了。这就是存储成本暴涨的根源。
我的建议:如果你预计未来会频繁加列,可以考虑:
- 预留一些“备用列”(比如 extra_col1, extra_col2),但别滥用
- 使用 JSON 列存储扩展属性(但要注意 JSON 的查询性能开销)
- 定期重建表(OPTIMIZE TABLE)回收碎片
3.4 实战:如何评估加列的成本
最后,我分享一个实用的评估方法。每次加列前,先问自己三个问题:
- 这个列真的需要吗? 能不能用已有列推导?能不能用应用层逻辑替代?
- 如果必须加,选 NULLABLE 还是 NOT NULL? 参考上面的表格,按数据量估算空间开销。
- 加列后会不会触发全表重建或页分裂? 如果会,评估窗口时间、磁盘空间、IO 压力。
我曾经帮一个客户做过评估:他们要在 10 亿行的订单表上加 3 个 NULLABLE 列。按我的估算,光 NULL 位图就要多占 10 亿 × 3 bit ≈ 375 MB。再加上数据页分裂导致的碎片,实际存储增长预计在 5 GB 左右。后来他们改成 2 个 NOT NULL + 1 个 JSON 列,存储增长控制在 1 GB 以内。
嗯,这就是细节的力量。加列看似简单,但每一列都在你的账单上留下了痕迹。