MySQL 8.4 中的 NULL 与 NOT NULL:语义、严格模式与安全迁移

NULL 表示“未知或缺失”,不是空字符串、数字零,也不是 JSON 文档里的 null 字面量。NOT NULL 是真实约束;旧文所说“在 MySQL 中已经不是约束”并不正确。让结果看似混乱的通常是会话 sql_mode、省略列、显式默认值、IGNORE、隐式转换,以及应用把不同状态显示成同一个空白。

本文以 MySQL 8.4 与 InnoDB 为中心。先在隔离环境复现,再变更应用和生产数据;不要靠关闭严格模式或覆盖整个 sql_mode 来让错误消失。

五种状态不能混为一谈

值或状态 含义 正确检查
SQL NULL 未知、缺失或不适用;不是一个普通值 col IS NULL
空字符串 '' 已知的零长度字符串 col = ''
数字 0 已知的数值零 col = 0
JSON null JSON 文档内存在的 null 标量 路径存在并且 JSON_TYPE(...) <=> 'NULL'
列被省略 INSERT 没有为该列提供表达式 由显式默认值、可空性与当前模式决定

业务模型必须先决定这些状态是否不同。例如“未提供备注”可以是 SQL NULL,“用户明确留空”可以是 '',重试次数为 0 则是有效数字。不要因为界面都显示为空白就合并语义。

用临时表安全观察差异

以下示例不含敏感数据,并使用当前会话的临时表;不要把它当成生产表设计模板:

CREATE TEMPORARY TABLE null_demo (
  id BIGINT UNSIGNED PRIMARY KEY,
  note VARCHAR(80) NULL,
  attempts INT NOT NULL DEFAULT 0,
  metadata JSON NULL
);

INSERT INTO null_demo (id, note, attempts, metadata) VALUES
  (1, NULL, 0, NULL),
  (2, '', 0, JSON_OBJECT('state', NULL)),
  (3, 'ready', 2, JSON_OBJECT('state', 'ready'));

同一张结果表可以同时分辨 SQL NULL、空字符串、零、JSON null 和缺失路径:

SELECT
  id,
  note IS NULL AS note_is_sql_null,
  note = '' AS note_is_empty,
  attempts = 0 AS attempts_is_zero,
  metadata IS NULL AS metadata_is_sql_null,
  JSON_CONTAINS_PATH(metadata, 'one', '$.state') AS state_path_exists,
  JSON_TYPE(JSON_EXTRACT(metadata, '$.state')) <=> 'NULL' AS state_is_json_null,
  JSON_CONTAINS_PATH(metadata, 'one', '$.missing') AS missing_path_exists
FROM null_demo
ORDER BY id;

MySQL 的 JSON 类型说明明确区分 SQL NULL 与 JSON null;JSON_TYPE 文档说明 JSON null 的类型结果是字符串 NULL,而 SQL NULL 参数产生 SQL NULL。检查路径是否存在,可避免把“路径缺失”误判为“路径存在且值为 JSON null”。

三值逻辑与 NULL 安全相等

普通比较遇到 NULL 通常产生 UNKNOWN,在结果中显示为 NULLWHERE 只保留结果为 TRUE 的行,因此 col = NULL 不能用于查找空值:

SELECT
  NULL = NULL AS ordinary_equal,
  NULL <=> NULL AS null_safe_equal,
  1 <=> NULL AS one_null_safe_equal;

SELECT id FROM null_demo WHERE note IS NULL;
SELECT id FROM null_demo WHERE note <=> NULL;

IS NULL 最适合明确判断空值。MySQL 特有的 <=> 是 NULL 安全相等:双方都为 NULL 时返回 1,只有一方为 NULL 时返回 0。它适合需要“包括 NULL 在内的相等”比较,但不要用它掩盖数据模型不清。NOT IN 的集合若含 NULL 也可能令条件变成 UNKNOWN;应先明确排除 NULL,或改用语义清楚的 NOT EXISTS

聚合函数怎样处理 NULL

COUNT(*) 统计行;COUNT(expr) 只统计表达式非 NULL 的行。SUMAVGMINMAX 等通常忽略 SQL NULL,而全为 NULL 或没有输入时的结果需按具体函数确认。一个安全的盘点写法是:

SELECT
  COUNT(*) AS total_rows,
  COUNT(note) AS non_null_notes,
  COUNT(*) - COUNT(note) AS null_notes,
  COALESCE(SUM(note = ''), 0) AS empty_notes,
  COALESCE(SUM(attempts = 0), 0) AS zero_attempt_rows
FROM null_demo;

不要把 COUNT(note) 当成总行数,也不要用 COALESCE 随意把未知业务值改成零;这里只用它把“空输入的计数结果”归一为计数值。

省略值、显式 NULL 与 DEFAULT

三种写法不同:省略列表示让服务器选择默认行为;写 DEFAULT 要求使用该列的默认;显式 NULL 要求写入 SQL NULL。先看真实 DDL,而不是根据管理界面的空白猜测:

CREATE TEMPORARY TABLE write_demo (
  id BIGINT UNSIGNED PRIMARY KEY,
  optional_note VARCHAR(80) NULL,
  required_note VARCHAR(80) NOT NULL,
  state VARCHAR(20) NOT NULL DEFAULT 'new'
);

INSERT INTO write_demo (id, required_note) VALUES (1, 'ready');
INSERT INTO write_demo (id, optional_note, required_note, state)
VALUES (2, NULL, 'ready', DEFAULT);

可空列没有显式默认时,MySQL 会把它定义为 DEFAULT NULLNOT NULL 列没有显式默认时,在严格模式下省略值或写入 NULL 会报错;非严格模式可能写入类型的隐式默认并产生 warning。AUTO_INCREMENT、生成列和表达式默认等有各自规则,必须按精确 DDL 与版本判断。

严格模式、IGNORE、错误与 warning

MySQL 8.4 的新安装通常启用严格模式,但迁移、托管服务、连接池和应用初始化语句都可能改变实际会话。始终检查当前连接和服务器全局值:

SELECT @@SESSION.sql_mode, @@GLOBAL.sql_mode;

SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.persisted_variables
WHERE VARIABLE_NAME = 'sql_mode';

严格模式下,InnoDB 的非法数据变更通常报错并回滚语句;非事务表可能发生部分写入,因此还要确认存储引擎。非严格模式可能调整值并给 warning。数据类型默认值文档描述了省略值与隐式默认的精确条件。

INSERT IGNOREUPDATE IGNORE 不是“通过验证”。IGNORE 会把某些错误降为 warning,跳过冲突行或把非法值调整为接近值;UPDATE IGNORE 还可能对 statement-based replication 不安全。迁移和数据修复不要用 IGNORE 隐藏问题。

任何可能调整数据的语句后都应立即读取诊断,因为下一条语句会覆盖它们:

SHOW COUNT(*) WARNINGS;
SHOW WARNINGS LIMIT 100;

warning 数为零、受影响行数符合预期、应用没有错误,三者都应进入验收证据。不要只看“查询成功”。

不要覆盖整个 sql_mode

直接执行 SET sql_mode = 'STRICT_TRANS_TABLES' 会删除 ONLY_FULL_GROUP_BY、日期检查和其他既有模式。先保存值,再用 MySQL sys.list_add() 只添加目标模式,并在测试后恢复当前会话:

SELECT @@SESSION.sql_mode, @@GLOBAL.sql_mode;

SET @previous_session_sql_mode = @@SESSION.sql_mode;
SET SESSION sql_mode = sys.list_add(@@SESSION.sql_mode, 'STRICT_TRANS_TABLES');
SELECT @@SESSION.sql_mode;

SET SESSION sql_mode = @previous_session_sql_mode;

如果 sys schema 不可用,应由 DBA 在变更系统中解析、去重和重组模式列表;不要手工复制一段可能已过期的默认字符串。

SESSION、GLOBAL 与 PERSIST 的边界

作用域 影响 重启后 重要限制
SESSION 仅当前连接 消失 连接池或应用可在新连接时重新设置
GLOBAL 后续新连接的默认值 消失 现有连接不变;需轮换连接才能观察
PERSIST 修改运行时全局值并写入 mysqld-auto.cnf 保留 需要管理权限,并应与配置管理协调
PERSIST_ONLY 只写下次启动值 保留 会制造当前与重启后差异,不适合临时试验

先生成并审阅候选值,不执行全局变更:

SET @previous_global_sql_mode = @@GLOBAL.sql_mode;
SET @candidate_sql_mode = sys.list_add(@previous_global_sql_mode, 'STRICT_TRANS_TABLES');
SELECT @previous_global_sql_mode, @candidate_sql_mode;

经应用兼容测试、变更审批、备份和回退演练后,只选择一种获批准的发布方式:

SET GLOBAL sql_mode = @candidate_sql_mode;
SET PERSIST sql_mode = @candidate_sql_mode;

这两行是互斥选项,不是顺序步骤。GLOBAL 仅用于非持久运行时发布;PERSIST 同时改运行时全局并持久化。对应回退也只执行曾选择的那一种:

SET GLOBAL sql_mode = @previous_global_sql_mode;
SET PERSIST sql_mode = @previous_global_sql_mode;

RESET PERSIST IF EXISTS sql_mode 只移除持久覆盖,不会自动恢复当前运行时值。生产变更还要确认应用是否自行设置会话模式、持久值来源、启动配置与副本一致性。

从 NULL 迁移到 NOT NULL

把已有列改成 NOT NULL 是应用和数据迁移,不只是一个 ALTER TABLE。安全顺序是:定义语义,盘点,更新应用写路径,分批回填,阻止新 NULL,验证副本和备份,最后再加约束。

1. 盘点 DDL、数据和依赖

保留完整列定义,特别是类型、长度、字符集、排序规则、默认、注释、生成表达式和索引。下例使用不敏感的示例表名:

SHOW CREATE TABLE customer_profile;

SELECT
  COUNT(*) AS total_rows,
  COUNT(display_name) AS non_null_rows,
  COUNT(*) - COUNT(display_name) AS null_rows,
  COALESCE(SUM(display_name = ''), 0) AS empty_rows
FROM customer_profile;

SELECT id
FROM customer_profile
WHERE display_name IS NULL
ORDER BY id
LIMIT 100;

同时盘点所有写入者、读取者、批处理、导入、触发器、外键、生成列、视图、CDC、备份、报表与副本。确认表为 InnoDB、主键可用于批次、备份可以恢复,并测量表大小、写入率、磁盘余量和复制延迟。

2. 先让应用兼容,再回填

先发布能读取旧 NULL、但不再写入新 NULL 的应用版本。为 NULL 选择业务上真实的替代来源,不能一律改成 ''0。例如只有 fallback_name 非空且已被业务批准时,才按可重复主键范围回填:

UPDATE customer_profile
SET display_name = fallback_name
WHERE id >= 10000
  AND id < 10500
  AND display_name IS NULL
  AND fallback_name IS NOT NULL
  AND fallback_name <> '';

SHOW COUNT(*) WARNINGS;
SHOW WARNINGS LIMIT 100;

每批都核对匹配行、变更行、warning、应用错误、锁等待与副本延迟。主键范围和批量大小必须根据真实数据选定;不要照抄示例数字。无法从可信来源推导的 NULL 应进入人工或业务决策队列,而不是伪造值。

3. 在受控窗口添加约束

应用已停止写 NULL、盘点为零且副本健康后,在具有生产相同 schema、数据规模和版本的环境演练精确 DDL。MODIFY 必须重写完整列定义;遗漏字符集、默认、注释等属性可能意外改变它们。

ALTER TABLE customer_profile
  MODIFY COLUMN display_name VARCHAR(120) NOT NULL,
  ALGORITHM=INPLACE,
  LOCK=NONE;

SHOW WARNINGS;

这里只是示例列定义。ALGORITHM=INPLACELOCK=NONE 会在引擎或操作不支持时失败,避免无声退回更强锁或表复制;它们不保证“无锁”。在线 DDL 仍会在开始和提交阶段取得元数据锁,可能重建表、消耗临时空间,并因并发写入、长事务或在线日志过大而失败。失败必须停下调查,不能删掉保护子句后直接重跑。

4. 复制、监控与上线闸门

变更前确认源与所有副本的 MySQL 版本、存储引擎、表定义、字符集、sql_mode、复制格式和 DDL 支持一致。暂停不必要的长事务,设置监控阈值,观察元数据锁、磁盘、I/O、CPU、错误日志、复制延迟与复制错误。

不要在副本上手工制造不同 schema,除非有经过验证的滚动 schema 方案。在线 DDL 可以允许并发 DML,但并发写入新的 NULL 可能让操作在末尾失败。应用写闸门、数据库约束变更和副本追平必须作为同一个发布流程。

验收与回退

约束完成后验证 schema、数据和运行中的会话:

SHOW CREATE TABLE customer_profile;

SELECT COUNT(*) AS remaining_nulls
FROM customer_profile
WHERE display_name IS NULL;

SELECT @@SESSION.sql_mode, @@GLOBAL.sql_mode;
SHOW WARNINGS;

还应执行应用读写、批处理、导入、备份恢复、故障切换与副本查询验收。只有全部通过后才结束维护窗口。

若必须撤回约束,先回退到同时兼容可空和非空的应用版本,再在受控窗口执行精确的反向 DDL:

ALTER TABLE customer_profile
  MODIFY COLUMN display_name VARCHAR(120) NULL,
  ALGORITHM=INPLACE,
  LOCK=NONE;

反向 DDL 也可能重建或锁表,而且不会恢复已回填前的 NULL。数据级回退需要经过验证的备份、变更日志或单独保存的映射。不要通过关闭严格模式来“回退”应用错误。

上线前检查表

  • SQL NULL''0、JSON null 和缺失路径已有业务定义。
  • 源、各副本和每类应用连接的 sql_mode 均已盘点,没有覆盖其他模式。
  • 省略值、显式 NULL、显式默认与 IGNORE 路径都有测试。
  • 应用先停止产生新 NULL,回填值有可信来源,剩余 NULL 为零。
  • 精确 DDL 已在等价环境演练,元数据锁、空间、时间与并发行为有证据。
  • 复制、CDC、备份恢复、监控、维护窗口与停止阈值已验证。
  • 回退应用、反向 DDL 和数据恢复分别有负责人;不假设 DDL 回退能还原数据。

官方资料

2011 年原文存档

以下惰性纯文本围栏保留 source_export 的完整可见正文。原文不含链接、个人数据、凭据或反斜线,因此没有安全删改;第 22 行末尾的 1 个 ASCII 空格已作尾随空白规范化。存档只是历史出处,其中“NOT NULL 已不是约束”“NULL 与空字符串基本一样”等说法已由维护层纠正,不是当前 MySQL 8.4 操作建议。

解决:MySQL中NULL和NOT NULL的混乱


本来,指定NOT NULL的意思是表中所有行的此属性必须有一个值。

如果没有指定或者指定为NULL,该列可以为空(NULL)。

但是,在MySQL中,你用NOT NULL,高版本的会自己解析默认值的:

- INT -> 0;
- CHAR -> **‘‘** (空值);
- DATATIME -> ‘0000-00-00 00:00:00’ 等等。

说白了,MySQL中的NOT NULL已经不是约束条件“表中所有行的此属性必须有一个值”了。并且如果字段是“字符型”,在DEFAULT情况下和NULL基本上是一样的。

**注意**:我在实际操作中发现,“表1″的“字段” password 是“字符型”,未指定(也就是指定为NULL);“字段” address 是“字符型”,指定为NOT NULL,在phpmyadmin中的显示如下:

表1

customerid name password address city 1 Julie Smith *NULL* Airport West

在 MySQL 中,为一个 NOT NULL 字段设置了一个 NULL 值,如果非STRICK模式,它并不会出错;但在SQL中却会出错。

MySQL 会自动将 NULL值转化为该字段的默认值, 那怕是你在表定义时**没有**明确地为该字段设置默认值,一般来说MySQL还是会自动为你添加默认值的,比如为一个 NOT NULL 的整型赋 NULL 值,结果是 0。

设为not null,不赋值的话字符串会为**‘‘**(空值),int会为0,不会出现错误。也就是说,设为not null,空值仍可以入库。

如果设为NULL,默认值为**‘‘**(空值)。

Leave a Reply