MySQL列属性从NULL改为NOT NULL DEFAULT的深坑与完美解决方案
作者:Seal^_^
引言
在MySQL数据库的日常运维中,我们经常需要修改表结构。其中一种常见的需求是:将列的属性从允许NULL改为NOT NULL并设置默认值。
ALTER TABLE slowtech.t1 MODIFY name VARCHAR(10) NOT NULL DEFAULT 'slowtech';
看似简单的操作,却可能引发严重的线上故障。本文将深入剖析这个操作背后的陷阱,并给出安全的实施步骤。
1. 问题现象
1.1 业务突然报错
某天,业务突然报错:
ERROR 1048 (23000): Column 'name' cannot be null
但奇怪的是,业务执行的SQL明明是:
INSERT INTO slowtech.t1(id) VALUES(1); -- 没有插入name字段!
按照常理,这条SQL不应该报错,因为:
- 原表
t1的name列允许NULL - 没有指定
name字段,应该自动填充NULL
1.2 现场排查
检查表结构:
mysql> DESC slowtech.t1; +-------+-------------+------+-----+----------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+-------------+------+-----+----------+-------+ | id | int(11) | NO | PRI | NULL | | | name | varchar(10) | NO | | slowtech | | -- 已改为NOT NULL +-------+-------------+------+-----+----------+-------+
原来,就在不久前,DBA执行了一个DDL操作,将name列从NULL改为了NOT NULL DEFAULT。
2. 深入分析:为什么会出现这个错误?
2.1 PT-OSC的实现原理
问题出在DDL工具的执行方式上。以pt-online-schema-change为例,其原理如下:



2.2 问题重现
-- 1. 原始表结构(允许NULL)
CREATE TABLE slowtech.t1(
id INT PRIMARY KEY,
name VARCHAR(10)
);
-- 2. PT-OSC创建新表并修改结构
CREATE TABLE slowtech._t1_new(
id INT PRIMARY KEY,
name VARCHAR(10) NOT NULL DEFAULT 'slowtech' -- 已修改
);
-- 3. 创建触发器
CREATE TRIGGER slowtech.`pt_osc_slowtech_t1_ins`
AFTER INSERT ON `slowtech`.`t1`
FOR EACH ROW
REPLACE INTO `slowtech`.`_t1_new` (`id`, `name`)
VALUES (NEW.`id`, NEW.`name`);
-- 4. 业务插入数据(只插入id)
INSERT INTO slowtech.t1(id) VALUES(1);
-- 报错:ERROR 1048 (23000): Column 'name' cannot be null
2.3 为什么会报错?


关键点:触发器会将业务SQL和触发操作放在同一个事务中执行。业务SQL本身不违反原表约束,但触发操作违反了新表的NOT NULL约束。
3. 数据拷贝阶段的另一个坑
3.1 拷贝过程中的报错
如果原表中已存在NULL值,PT-OSC在拷贝数据时也会报错:
-- 原表存在NULL值 mysql> INSERT INTO slowtech.t1(id) VALUES(1); mysql> SELECT * FROM t1; +----+------+ | id | name | +----+------+ | 1 | NULL | +----+------+ -- 执行PT-OSC pt-online-schema-change h=localhost,D=slowtech,t=t1 \ --alter "MODIFY name VARCHAR(10) NOT NULL DEFAULT 'slowtech'" \ --execute -- 报错: -- Error copying rows from `slowtech`.`t1` to `slowtech`.`_t1_new`: -- Column 'name' cannot be null
3.2 使用–null-to-not-null参数的隐患
-- 添加参数可以忽略1048错误 pt-online-schema-change ... --null-to-not-null --execute -- 实际执行的是: INSERT LOW_PRIORITY IGNORE INTO `slowtech`.`_t1_new` (`id`, `name`) SELECT `id`, `name` FROM `slowtech`.`t1` LOCK IN SHARE MODE; -- 查看结果 mysql> SELECT * FROM _t1_new; +----+------+ | id | name | +----+------+ | 1 | | -- NULL被转换为空字符串! +----+------+
⚠️ 重要警告:--null-to-not-null会将NULL值转换为:
- 字符类型:空字符串
'' - 数字类型:
0 - 日期类型:
'0000-00-00'
这些默认值是否符合业务预期,必须仔细确认!
4. 正确的实施步骤
4.1 总体流程

4.2 详细实施步骤
步骤1:先改为NULL DEFAULT
-- 先允许NULL,但设置默认值 ALTER TABLE slowtech.t1 MODIFY name VARCHAR(10) NULL DEFAULT 'slowtech';
作用:确保增量数据不再产生新的NULL值(未指定该列时会填充默认值)。
步骤2:处理现有NULL值
-- 查看现有NULL值数量 SELECT COUNT(*) FROM slowtech.t1 WHERE name IS NULL; -- 分批更新NULL值为默认值(避免锁大表) -- 每次更新1000条 UPDATE slowtech.t1 SET name = DEFAULT WHERE name IS NULL LIMIT 1000; -- 确认没有NULL值了 SELECT COUNT(*) FROM slowtech.t1 WHERE name IS NULL;
注意事项:
- 对于大表,务必分批更新
- 选择业务低峰期执行
- 监控主从延迟和数据库负载
步骤3:最终改为NOT NULL DEFAULT
-- 确认没有NULL值后,执行最终修改 ALTER TABLE slowtech.t1 MODIFY name VARCHAR(10) NOT NULL DEFAULT 'slowtech';
4.3 使用PT-OSC的安全方案
如果必须使用PT-OSC,可以这样操作:
# 第一步:先改为NULL DEFAULT pt-online-schema-change h=localhost,D=slowtech,t=t1 \ --alter "MODIFY name VARCHAR(10) NULL DEFAULT 'slowtech'" \ --execute # 第二步:手动更新NULL值(同上) mysql -e "UPDATE slowtech.t1 SET name = DEFAULT WHERE name IS NULL LIMIT 1000" # 第三步:改为NOT NULL DEFAULT pt-online-schema-change h=localhost,D=slowtech,t=t1 \ --alter "MODIFY name VARCHAR(10) NOT NULL DEFAULT 'slowtech'" \ --execute
5. 理解NOT NULL DEFAULT的真正含义
很多开发者对NOT NULL DEFAULT的理解存在误区:
-- 创建表
CREATE TABLE t1 (
id INT PRIMARY KEY,
name VARCHAR(10) NOT NULL DEFAULT 'slowtech'
);
5.1 误区一:认为DEFAULT会覆盖NULL
-- ❌ 错误理解:认为会自动将NULL转为默认值 INSERT INTO t1 VALUES (1, NULL); -- 实际结果:ERROR 1048 (23000): Column 'name' cannot be null
5.2 误区二:认为NOT NULL和DEFAULT是一体的
-- ✅ 正确理解:这是两个独立的部分
CREATE TABLE t2 (
name1 VARCHAR(10) NOT NULL DEFAULT 'a', -- NOT NULL + DEFAULT
name2 VARCHAR(10) NULL DEFAULT 'b', -- NULL + DEFAULT
name3 VARCHAR(10) NOT NULL, -- NOT NULL + 无DEFAULT
name4 VARCHAR(10) NULL -- NULL + 无DEFAULT
);
5.3 各组合的实际效果
| 定义 | 插入NULL | 不指定该列 | 插入其他值 |
|---|---|---|---|
NOT NULL DEFAULT 'a' | ❌ 报错 | ✅ 填’a’ | ✅ 正常 |
NULL DEFAULT 'a' | ✅ 填NULL | ✅ 填’a’ | ✅ 正常 |
NOT NULL | ❌ 报错 | ❌ 报错 | ✅ 正常 |
NULL | ✅ 填NULL | ✅ 填NULL | ✅ 正常 |
6. PT-OSC使用注意事项
6.1 异常退出后的清理顺序
如果PT-OSC异常退出,留下了触发器和中间表,清理时必须注意顺序:
-- ❌ 错误顺序:先删中间表 DROP TABLE slowtech._t1_new; -- 此时业务执行DML会报错 INSERT INTO slowtech.t1 VALUES (1, 'victor'); -- ERROR 1146 (42S02): Table 'slowtech._t1_new' doesn't exist -- ✅ 正确顺序:先删触发器 DROP TRIGGER slowtech.`pt_osc_slowtech_t1_ins`; -- 再删中间表 DROP TABLE slowtech._t1_new;
6.2 关键参数说明
| 参数 | 作用 | 风险 |
|---|---|---|
--null-to-not-null | 忽略1048错误,将NULL转为默认值 | 转换后的值可能不符合预期 |
--no-swap-tables | 只创建新表不交换 | 用于调试,不会修改原表 |
--chunk-size | 控制每次拷贝的行数 | 太小影响速度,太大影响性能 |
7. 最佳实践总结
7.1 安全三步曲

7.2 操作检查清单
- 确认业务是否能接受短暂的NULL值存在
- 评估NULL值的数量,制定分批更新计划
- 选择业务低峰期执行
- 监控数据库负载和主从延迟
- 准备好回滚方案
7.3 一句话总结
将列从NULL改为NOT NULL DEFAULT,真正的难点不在于DDL本身,而在于如何安全地处理现有NULL值和增量数据中的潜在NULL值。
理解这一点,才能避免踩坑,安全地完成这个看似简单的操作。
到此这篇关于MySQL列属性从NULL改为NOT NULL DEFAULT的深坑与完美解决方案的文章就介绍到这了,更多相关mysql列属性从null改为not null default内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
