avatar

Ryan's Blog

The first step is always the hardest.

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于
  • 工具
Home MySQL AUTO_INCREMENT 插入 0 变成自增值的原因与解决方案
文章

MySQL AUTO_INCREMENT 插入 0 变成自增值的原因与解决方案

Posted 2024-09-4 Updated 昨天
By Ryan Chen
19~25 min read

MySQL AUTO_INCREMENT 插入 0 变成自增值的原因与解决方案

前言

在 MySQL 中,AUTO_INCREMENT 常用于生成自增主键。正常情况下,我们只需要在插入数据时省略主键,或者把主键设置为 NULL,MySQL 就会自动生成下一个自增 ID。

但在一些特殊场景中,例如初始化默认分类、兼容旧系统数据、迁移历史数据时,可能需要插入一条 id = 0 的记录。这时很多人会发现:明明 SQL 里写了 0,最终落库的却是另一个自增值。

本文记录这个问题的原因、复现方式和几种处理方案。

问题现象

假设有一张配置分类表,id 是自增主键:

CREATE TABLE config_type (
  id BIGINT NOT NULL AUTO_INCREMENT,
  type VARCHAR(64) NOT NULL,
  name VARCHAR(128) NOT NULL,
  parent_id BIGINT NULL,
  PRIMARY KEY (id)
) ENGINE = InnoDB;

现在希望插入一条 ID 为 0 的默认分类:

INSERT INTO config_type (id, type, name, parent_id)
VALUES (0, 'service', '默认分类', NULL);

预期结果:

id = 0

实际结果可能是:

id = 1

也就是说,0 没有作为普通值写入,而是触发了自增 ID 的生成。

原因分析:0 和 NULL 默认都会触发自增

MySQL 对 AUTO_INCREMENT 列有一个特殊规则:在默认 SQL Mode 下,向自增列插入 NULL 或 0,MySQL 都会生成下一个序列值。

也就是说,下面几种写法在默认情况下都可能触发自增:

-- 省略自增列
INSERT INTO config_type (type, name) VALUES ('service', '默认分类');

-- 显式插入 NULL
INSERT INTO config_type (id, type, name) VALUES (NULL, 'service', '默认分类');

-- 显式插入 0,默认也会触发自增
INSERT INTO config_type (id, type, name) VALUES (0, 'service', '默认分类');

如果希望 0 被当作普通数值插入,而不是触发自增,就需要启用 SQL Mode:NO_AUTO_VALUE_ON_ZERO。

启用该模式后:

  • 插入 NULL:仍然生成新的自增值;
  • 插入 0:按普通值 0 写入表中。

复现实验

可以用下面的临时表复现这个行为:

DROP TABLE IF EXISTS t_auto_zero;

CREATE TABLE t_auto_zero (
  id INT NOT NULL AUTO_INCREMENT,
  name VARCHAR(32) NOT NULL,
  PRIMARY KEY (id)
) ENGINE = InnoDB;

默认模式下插入 0:

INSERT INTO t_auto_zero (id, name) VALUES (0, 'zero');
SELECT id, name FROM t_auto_zero;

可能得到:

+----+------+
| id | name |
+----+------+
|  1 | zero |
+----+------+

此时 0 被 MySQL 解释成“请生成下一个自增值”。

解决方案一:当前会话开启 NO_AUTO_VALUE_ON_ZERO

如果只是当前连接、当前脚本需要插入 0,推荐使用会话级设置:

SELECT @@SESSION.sql_mode;

SET @old_sql_mode = @@SESSION.sql_mode;

SET SESSION sql_mode = CASE
  WHEN FIND_IN_SET('NO_AUTO_VALUE_ON_ZERO', @@SESSION.sql_mode) > 0
    THEN @@SESSION.sql_mode
  WHEN @@SESSION.sql_mode = ''
    THEN 'NO_AUTO_VALUE_ON_ZERO'
  ELSE CONCAT(@@SESSION.sql_mode, ',NO_AUTO_VALUE_ON_ZERO')
END;

INSERT INTO config_type (id, type, name, parent_id)
VALUES (0, 'service', '默认分类', NULL);

SET SESSION sql_mode = @old_sql_mode;

这种方式的优点是影响范围小,只对当前数据库连接生效,适合一次性修复、初始化脚本或数据导入脚本。

解决方案二:全局开启 SQL Mode

如果业务上长期需要允许 AUTO_INCREMENT 字段插入 0,也可以设置全局 SQL Mode:

SELECT @@GLOBAL.sql_mode;

SET GLOBAL sql_mode = CASE
  WHEN FIND_IN_SET('NO_AUTO_VALUE_ON_ZERO', @@GLOBAL.sql_mode) > 0
    THEN @@GLOBAL.sql_mode
  WHEN @@GLOBAL.sql_mode = ''
    THEN 'NO_AUTO_VALUE_ON_ZERO'
  ELSE CONCAT(@@GLOBAL.sql_mode, ',NO_AUTO_VALUE_ON_ZERO')
END;

需要注意:

  1. SET GLOBAL 通常需要较高权限;
  2. 它不会影响已经建立的连接,只影响之后新建的连接;
  3. 生产环境不要为了单次导入随意修改全局 SQL Mode;
  4. 修改前应先记录原始 @@GLOBAL.sql_mode,避免覆盖已有模式。

如果使用 MySQL 8.0+,也可以结合 SET PERSIST 做持久化配置,但仍然建议先评估影响范围。

解决方案三:配置文件持久化

如果确认整个实例都需要长期启用该模式,可以在 MySQL 配置文件中设置。

Linux 常见配置文件:

/etc/my.cnf
/etc/mysql/my.cnf
/etc/mysql/mysql.conf.d/mysqld.cnf

示例:

[mysqld]
sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION,NO_AUTO_VALUE_ON_ZERO

配置后需要重启 MySQL 才会生效。

注意不要只写:

sql_mode=NO_AUTO_VALUE_ON_ZERO

否则可能会把原有的严格模式、日期检查、引擎替代检查等 SQL Mode 覆盖掉,导致其他行为发生变化。

数据导入场景建议

如果需求来自数据迁移或初始化脚本,建议采用下面的模式:

SET @old_sql_mode = @@SESSION.sql_mode;

SET SESSION sql_mode = CASE
  WHEN FIND_IN_SET('NO_AUTO_VALUE_ON_ZERO', @@SESSION.sql_mode) > 0
    THEN @@SESSION.sql_mode
  WHEN @@SESSION.sql_mode = ''
    THEN 'NO_AUTO_VALUE_ON_ZERO'
  ELSE CONCAT(@@SESSION.sql_mode, ',NO_AUTO_VALUE_ON_ZERO')
END;

-- 执行需要保留 id = 0 的导入语句
INSERT INTO config_type (id, type, name, parent_id)
VALUES (0, 'service', '默认分类', NULL);

SET SESSION sql_mode = @old_sql_mode;

这样既能保证导入时 0 被正确写入,也不会长期改变应用连接的行为。

是否推荐使用 ID = 0?

虽然可以通过 NO_AUTO_VALUE_ON_ZERO 插入 id = 0,但不建议在普通业务数据中频繁使用 0 作为主键。

更推荐的做法是:

  • 如果表示“无父级”,优先使用 parent_id IS NULL;
  • 如果表示“默认分类”,可以使用单独的业务字段,例如 is_default;
  • 如果表示“系统内置数据”,可以使用独立编码字段,例如 code = 'default';
  • 如果是历史系统兼容或数据迁移,则可以谨慎保留 id = 0。

换句话说:

NO_AUTO_VALUE_ON_ZERO 更适合解决迁移、兼容、初始化场景,不应成为日常业务建模依赖。

常见问题

1. 为什么我执行 SET GLOBAL 后还是插入不了 0?

SET GLOBAL 不会影响当前已经存在的连接。你需要重新建立数据库连接,或者直接执行 SET SESSION sql_mode = ...。

2. 可以直接把 sql_mode 设置成 NO_AUTO_VALUE_ON_ZERO 吗?

不建议。sql_mode 往往包含多个模式,直接覆盖可能会关闭原有的严格检查。更安全的方式是在原有值基础上追加 NO_AUTO_VALUE_ON_ZERO。

3. 插入 0 会不会影响后续自增值?

通常不会让自增序列从 0 开始。后续插入 NULL 或省略自增列时,MySQL 仍然会继续生成新的自增值。

4. 所有数据库都这样处理自增列的 0 吗?

不是。这是 MySQL AUTO_INCREMENT 与 SQL Mode 组合下的行为。迁移到 PostgreSQL、Oracle、SQL Server 等数据库时,需要重新确认对应数据库的自增列、序列或 identity column 行为。

总结

MySQL 自增 ID 插入 0 后变成其他自增值,并不是插入语句失效,而是 AUTO_INCREMENT 的默认规则导致的:默认情况下,NULL 和 0 都会触发生成下一个自增值。

如果确实需要保存 id = 0,可以启用:

NO_AUTO_VALUE_ON_ZERO

实践建议是:

  • 临时导入或修复数据:优先使用 SET SESSION;
  • 长期实例级策略:谨慎使用 SET GLOBAL 或配置文件;
  • 普通业务建模:尽量避免依赖 0 作为特殊主键值。

相关阅读与参考资料

  • B+树原理与 MySQL InnoDB 索引机制解析
  • MySQL 官方文档:Server SQL Modes
  • MySQL 官方文档:NO_AUTO_VALUE_ON_ZERO
  • MySQL 官方文档:Using AUTO_INCREMENT
  • MySQL 官方文档:SET Syntax for Variable Assignment
指南
MySQL AUTO_INCREMENT SQL Mode NO_AUTO_VALUE_ON_ZERO 数据迁移 问题排查
License:  CC BY 4.0
Share

Further Reading

Jun 27, 2026

Agent 架构设计原则:Router、Runtime 与 Business Script 的职责划分

本文整理一套适合 Router Agent + Skill + Runtime 架构的设计原则:Agent 只负责业务决策,Runtime 统一负责执行、恢复、Trace、Checkpoint 和 Evidence,Business Script 只做确定性业务执行。

Sep 9, 2024

Redis 核心概念、数据结构与高可用架构详解

系统整理 Redis 的核心概念、常用数据结构、缓存场景、持久化机制和高可用架构,适合作为 Redis 学习与面试复习材料。

Sep 5, 2024

B+树原理与 MySQL InnoDB 索引机制解析

本文从 B+ 树的多叉平衡结构、叶子节点链表、范围查询和磁盘 I/O 特性出发,解释数据库索引为什么常采用 B+ 树,并结合 MySQL InnoDB 的聚簇索引、二级索引、回表、覆盖索引和联合索引机制理解其实际应用。

OLDER

B+树原理与 MySQL InnoDB 索引机制解析

NEWER

LSM Tree 存储结构解析:写入密集场景下的数据组织与压缩

Recently Updated

  • Agent 架构设计原则:Router、Runtime 与 Business Script 的职责划分
  • RocketMQ 架构设计与应用最佳实践:高可用消息队列核心解析
  • Redis 核心概念、数据结构与高可用架构详解
  • B+树原理与 MySQL InnoDB 索引机制解析
  • MySQL AUTO_INCREMENT 插入 0 变成自增值的原因与解决方案

Trending Tags

RocketMQ Windows Feign Docker Zipkin SonarQube OkHttp HttpClient API 性能优化

Contents

©2026 Ryan's Blog. Some rights reserved. · 粤ICP备2022031588号