返回文章列表 →

Note

约束

记录 MySQL 字段约束、外键约束及引用行为的语法和示例

发布于 更新于

约束

约束(Constraint)是由数据库强制执行的数据规则,用于阻止不符合要求的写入,保证数据的有效性、一致性和完整性。与只在应用代码中校验相比,约束会统一作用于所有写入数据库的入口。

约束不一定只作用于单个字段:它既可以定义在字段上,也可以作为表级规则约束一个或多个字段;外键约束还会维护表与表之间或同一张表内部的引用关系。

本文以 MySQL 8.0.16 及之后的版本、InnoDB 存储引擎和严格 SQL 模式为基准,介绍两类常用约束:

  • 字段约束:限制字段值是否允许为空、是否重复、是否满足条件等。
  • 外键约束:保证子表中的引用值能够在父表中找到对应记录。

字段约束

字段约束通常在创建表时声明,也可以在表创建后使用 ALTER TABLE 添加、修改或删除。

规则或属性 关键字 作用
非空约束 NOT NULL 禁止字段保存 SQL 的 NULL 值
唯一约束 UNIQUE 保证一个字段或字段组合中的非空值不重复
主键约束 PRIMARY KEY 唯一标识一行记录,主键值必须唯一且不能为 NULL
默认值属性 DEFAULT 插入时省略字段或显式写 DEFAULT,使用预先设置的值
检查约束 CHECK 要求字段值满足指定的布尔表达式
自动增长属性 AUTO_INCREMENT 为整数索引字段自动生成递增编号

其中,DEFAULT 和 AUTO_INCREMENT 更准确地说是字段属性,而不是负责拒绝非法值的完整性约束。

非空约束

NULL 表示未知或缺失的值,不等于空字符串 ''、数字 0 或字符串 'NULL'。因此,NOT NULL 不会自动拒绝这些值;

字段名 数据类型 NOT NULL

唯一约束

UNIQUE 保证一个字段或字段组合中的非空值不重复。MySQL 使用唯一索引实现唯一约束。允许为 NULL 的唯一字段可以保存多个 NULL。

-- 单字段的字段级写法
字段名 数据类型 UNIQUE

-- 单字段或多字段的表级写法
CONSTRAINT 唯一约束名 UNIQUE (字段名1, 字段名2, ...)

主键约束

PRIMARY KEY 用于唯一标识表中的每一行记录,同时具有非空和唯一的性质。一张表只能有一个主键,但这个主键可以由一个或多个字段组成。

-- 单字段的字段级写法
字段名 数据类型 PRIMARY KEY

-- 单字段或联合主键的表级写法
PRIMARY KEY (字段名1, 字段名2, ...)

联合主键判断的是字段组合是否重复。例如,主键为 (student_id, course_id) 时,同一名学生可以选择多门课程,同一门课程也可以被多名学生选择,但同一个“学生与课程”组合不能重复。

默认值属性

DEFAULT 为字段设置默认值。默认值只在插入记录时省略该字段,或显式写入 DEFAULT 时使用。如果字段允许为 NULL,显式写入 NULL 保存的仍然是 NULL,不会自动替换为默认值。

字段名 数据类型 DEFAULT 默认值

检查约束

CHECK 要求每一行数据满足指定条件,可以限制数值范围、状态集合或多个字段之间的关系。

-- 字段级写法,只能引用当前字段
字段名 数据类型 CHECK (条件表达式)

-- 表级写法,可以引用一个或多个字段
CONSTRAINT 检查约束名 CHECK (条件表达式)

检查表达式的结果为 FALSE 时写入失败,为 TRUE 或 UNKNOWN 时可以通过。涉及 NULL 的比较通常得到 UNKNOWN,因此 CHECK (age BETWEEN 1 AND 120) 本身仍允许 age 为 NULL;如果年龄必须填写,还要添加 NOT NULL。

自动增长属性

AUTO_INCREMENT 为新增记录生成递增编号,通常和整数主键一起使用。每张表最多只能有一个自动增长字段,该字段必须建立索引,并且不能再声明普通的 DEFAULT 值。插入时省略该字段或写入 NULL,MySQL 会生成下一个编号;自动增长值只保证自动生成时不会与已有值冲突,不保证永久连续。插入失败、事务回滚、删除记录或显式写入较大编号,都可能让编号之间出现空缺。

字段名 整数类型 AUTO_INCREMENT

添加和删除字段约束

在已有表上添加或删除常用规则的基本语法如下:

-- 修改字段的非空和自动增长属性
ALTER TABLE 表名
MODIFY COLUMN 字段名 完整数据类型 [NULL | NOT NULL] [AUTO_INCREMENT];

-- 设置或删除默认值
ALTER TABLE 表名 ALTER COLUMN 字段名 SET DEFAULT 默认值;
ALTER TABLE 表名 ALTER COLUMN 字段名 DROP DEFAULT;

-- 添加表级约束
ALTER TABLE 表名 ADD PRIMARY KEY (字段列表);
ALTER TABLE 表名 ADD CONSTRAINT 唯一约束名 UNIQUE (字段列表);
ALTER TABLE 表名 ADD CONSTRAINT 检查约束名 CHECK (条件表达式);

-- 删除表级约束
ALTER TABLE 表名 DROP PRIMARY KEY;
ALTER TABLE 表名 DROP INDEX 唯一约束名;
ALTER TABLE 表名 DROP CHECK 检查约束名;

MODIFY COLUMN 需要重新写出完整的字段定义。修改时如果遗漏原有的 NOT NULL、DEFAULT、AUTO_INCREMENT 等属性,这些属性可能被移除。

CREATE TABLE、ALTER TABLE 和 DROP TABLE 属于 DDL 语句,在 MySQL 中通常会隐式提交事务,不能依靠后续的 ROLLBACK 撤销。

外键约束

外键约束(Foreign Key Constraint)用于维护引用完整性。保存被引用值(引用值作主键)的表称为父表,保存外键的表称为子表;子表中的每个非空外键值,都必须在父表被引用字段中存在。

行为 作用
NO ACTION 默认行为;存在匹配的子表记录时拒绝更新或删除父表记录
RESTRICT 立即拒绝更新或删除父表记录;在 InnoDB 中与 NO ACTION 效果相同
CASCADE 父表键更新时同步更新子表外键;父表记录删除时同步删除匹配的子表记录
SET NULL 父表键更新或记录删除时,将匹配的子表外键设置为 NULL;外键字段必须允许为 NULL
SET DEFAULT InnoDB 没有实现“把子表外键设为默认值”的可用行为,不应使用

当父表被引用键发生更新,或父表记录被删除时,ON UPDATE 和 ON DELETE 会通过“行为”决定如何处理匹配的子表记录。省略 ON UPDATE 或 ON DELETE 时,对应行为默认为 NO ACTION。InnoDB 不支持延迟检查,因此它与 RESTRICT 一样,会在语句执行时立即拒绝破坏引用完整性的操作。

创建外键

创建子表时可以直接声明外键:

CREATE TABLE 子表名 (
    字段定义,
    ...,
    [CONSTRAINT [外键名称]]
        FOREIGN KEY (子表外键字段)
        REFERENCES 父表名 (父表被引用字段)
        [ON DELETE 引用行为]
        [ON UPDATE 引用行为]
);

也可以为已有表添加外键:

ALTER TABLE 子表名
    ADD [CONSTRAINT [外键名称]]
    FOREIGN KEY (子表外键字段)
    REFERENCES 父表名 (父表被引用字段)
    [ON DELETE 引用行为]
    [ON UPDATE 引用行为];

语法中的方括号表示可选部分,实际编写 SQL 时不输入方括号。建议显式命名外键,方便后续删除和排查问题。

外键定义应使用独立的表级 FOREIGN KEY 子句,不要只在字段定义后书写内联的 REFERENCES。

创建条件

创建 InnoDB 外键时需要注意:

  1. 引用其他表时,父表结构必须先存在,但父表可以暂时没有数据。插入或更新非空子表外键前,对应的父表记录必须存在;自引用外键则可以在当前表的建表语句中直接定义。
  2. 父表和子表应使用相同且支持外键的存储引擎,本文统一使用 InnoDB。
  3. 外键两端的字段类型必须兼容。整数类型的大小和是否为 UNSIGNED 应一致;非二进制字符串的字符集和排序规则必须一致。
  4. 实际设计中应让外键引用父表的主键或 UNIQUE NOT NULL 键,避免依赖非标准的非唯一键引用。
  5. 子表外键字段需要索引;InnoDB 会在缺少可用索引时自动创建。
  6. 为已有表添加外键时,现有的每个非空外键值都必须能够匹配父表记录,否则添加失败。
  7. 子表外键字段如果允许为 NULL,NULL 不需要匹配父表中的记录;如果每条子表记录都必须属于某个父表记录,应再添加 NOT NULL。

删除外键

ALTER TABLE 子表名
DROP FOREIGN KEY 外键名称;

DROP FOREIGN KEY 后面填写的是外键约束名。如果创建时没有显式命名,可以先查看建表语句:

SHOW CREATE TABLE 子表名;

删除外键约束后,InnoDB 为它创建的索引可能仍然保留。可以使用 SHOW INDEX FROM 子表名 检查,并在确定没有其他用途后单独删除该索引。

示例

数据准备

下面先创建用于演示字段约束的用户表,再创建 dept 部门表和 emp 员工表。准备阶段暂不为 emp.dept_id 添加外键,以便后续分别演示创建表时添加和使用 ALTER TABLE 添加外键。

以下代码会删除并重新创建同名演示表,请仅在练习数据库中执行。重复执行时必须先删除子表,再删除父表。

DROP TABLE IF EXISTS set_default_demo;
DROP TABLE IF EXISTS emp_create_fk_demo;
DROP TABLE IF EXISTS emp;
DROP TABLE IF EXISTS dept;
DROP TABLE IF EXISTS field_change_demo;
DROP TABLE IF EXISTS user_constraint_demo;

CREATE TABLE user_constraint_demo (
    id INT UNSIGNED AUTO_INCREMENT COMMENT 'ID',
    name VARCHAR(10) NOT NULL COMMENT '姓名',
    age INT COMMENT '年龄',
    status CHAR(1) DEFAULT '1' COMMENT '状态',
    gender CHAR(1) COMMENT '性别',
    PRIMARY KEY (id),
    CONSTRAINT uk_user_constraint_name UNIQUE (name),
    CONSTRAINT chk_user_constraint_age
        CHECK (age > 0 AND age <= 120)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '字段约束演示表';

CREATE TABLE dept (
    id INT AUTO_INCREMENT COMMENT 'ID',
    name VARCHAR(50) NOT NULL COMMENT '部门名称',
    PRIMARY KEY (id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '部门表';

INSERT INTO dept (id, name)
VALUES
    (1, '研发部'),
    (2, '市场部'),
    (3, '财务部'),
    (4, '销售部'),
    (5, '总经办');

CREATE TABLE emp (
    id INT AUTO_INCREMENT COMMENT 'ID',
    name VARCHAR(50) NOT NULL COMMENT '姓名',
    age INT COMMENT '年龄',
    job VARCHAR(20) COMMENT '职位',
    salary INT COMMENT '薪资',
    entrydate DATE COMMENT '入职时间',
    managerid INT COMMENT '直属领导ID',
    dept_id INT COMMENT '部门ID',
    PRIMARY KEY (id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '员工表';

INSERT INTO emp
    (id, name, age, job, salary, entrydate, managerid, dept_id)
VALUES
    (1, '金庸', 66, '总裁', 20000, '2000-01-01', NULL, 5),
    (2, '张无忌', 20, '项目经理', 12500, '2005-12-05', 1, 1),
    (3, '杨逍', 33, '开发', 8400, '2000-11-03', 2, 1),
    (4, '韦一笑', 48, '开发', 11000, '2002-02-05', 2, 1),
    (5, '常遇春', 43, '开发', 10500, '2004-09-07', 3, 1),
    (6, '小昭', 19, '程序员鼓励师', 6600, '2004-10-12', 2, 1);

managerid 保存直属领导编号,但本节只围绕 emp.dept_id -> dept.id 展开,因此不再额外创建自引用外键。

标有“预期失败”的语句用于观察数据库报错,请逐条执行。部分客户端会在批量执行遇到第一个错误后停止,不再继续运行同一批次中的后续语句。

字段约束

合法数据与字段属性

下面三条记录分别演示省略默认值字段、显式使用 DEFAULT、覆盖默认值,以及年龄检查的上下边界:

INSERT INTO user_constraint_demo (name, age, gender)
VALUES ('张三', 20, '男');

INSERT INTO user_constraint_demo (name, age, status, gender)
VALUES ('李四', 120, DEFAULT, NULL);

INSERT INTO user_constraint_demo (name, age, status, gender)
VALUES ('王五', 1, '0', '女');

SELECT id, name, age, status, gender
FROM user_constraint_demo
ORDER BY id;

三条记录的 id 由 AUTO_INCREMENT 自动生成。张三省略了 status,李四显式写入 DEFAULT,两人的状态都会使用默认值 '1';王五显式写入 '0',因此不会使用默认值。gender 没有非空约束,所以李四的性别可以为 NULL。

NOT NULL

显式向 name 写入 NULL 会违反非空约束,下面的语句预期执行失败:

INSERT INTO user_constraint_demo (name, age)
VALUES (NULL, 20);

空字符串不是 NULL,因此下面的插入可以成功。事务回滚用于撤销演示数据:

START TRANSACTION;

INSERT INTO user_constraint_demo (name, age)
VALUES ('', 20);

SELECT id, name, age
FROM user_constraint_demo
WHERE name = '';

ROLLBACK;
UNIQUE

name 同时具有 NOT NULL 和 UNIQUE 约束。再次写入“张三”会违反唯一约束,下面的语句预期执行失败:

INSERT INTO user_constraint_demo (name, age)
VALUES ('张三', 30);

如果唯一字段允许为 NULL,MySQL 可以保存多个 NULL:

DROP TEMPORARY TABLE IF EXISTS unique_null_demo;

CREATE TEMPORARY TABLE unique_null_demo (
    id INT AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(10) UNIQUE
);

INSERT INTO unique_null_demo (code)
VALUES (NULL), (NULL), ('A001');

SELECT id, code
FROM unique_null_demo
ORDER BY id;

再次写入相同的非空值仍会失败:

-- 预期失败:A001 已经存在
INSERT INTO unique_null_demo (code)
VALUES ('A001');
DROP TEMPORARY TABLE unique_null_demo;
PRIMARY KEY

显式写入已经存在的主键值 1 会失败:

-- 预期失败:主键 1 已经存在
INSERT INTO user_constraint_demo (id, name, age)
VALUES (1, '赵六', 30);

因为 user_constraint_demo.id 同时具有 AUTO_INCREMENT 属性,向它写入 NULL 会生成新编号,而不是触发主键非空错误。下面使用联合主键单独演示主键组合的唯一和非空规则:

DROP TEMPORARY TABLE IF EXISTS course_selection_demo;

CREATE TEMPORARY TABLE course_selection_demo (
    student_id INT,
    course_id INT,
    PRIMARY KEY (student_id, course_id)
);

INSERT INTO course_selection_demo (student_id, course_id)
VALUES (1, 1), (1, 2), (2, 1);

SELECT student_id, course_id
FROM course_selection_demo
ORDER BY student_id, course_id;

再次写入相同的主键组合会失败:

-- 预期失败:(1, 1) 已经存在
INSERT INTO course_selection_demo (student_id, course_id)
VALUES (1, 1);

联合主键中的任意字段为 NULL 也会失败:

-- 预期失败:联合主键中的字段不能为 NULL
INSERT INTO course_selection_demo (student_id, course_id)
VALUES (NULL, 3);
DROP TEMPORARY TABLE course_selection_demo;
DEFAULT

前面的合法数据已经覆盖了省略 status、显式写 DEFAULT 和显式覆盖默认值三种情况。下面演示显式写入 NULL:

START TRANSACTION;

INSERT INTO user_constraint_demo (name, age, status)
VALUES ('赵六', 30, NULL);

SELECT name, status, status IS NULL AS is_null
FROM user_constraint_demo
WHERE name = '赵六';

ROLLBACK;

查询中的 is_null 返回 1,说明显式写入的 NULL 没有被默认值 '1' 替换。

CHECK

数据准备中的三条合法记录已经覆盖年龄 1 和 120 两个边界。年龄为 0 时低于下限,下面的语句预期执行失败:

-- 预期失败:年龄不能为 0
INSERT INTO user_constraint_demo (name, age)
VALUES ('年龄过小', 0);

年龄为 121 时高于上限,同样会失败:

-- 预期失败:年龄不能大于 120
INSERT INTO user_constraint_demo (name, age)
VALUES ('年龄过大', 121);

NULL 参与比较时,检查表达式得到 UNKNOWN,所以当前定义允许年龄未知:

START TRANSACTION;

INSERT INTO user_constraint_demo (name, age)
VALUES ('年龄未知', NULL);

SELECT name, age
FROM user_constraint_demo
WHERE name = '年龄未知';

ROLLBACK;

如果年龄必须填写,应把字段定义改为 age INT NOT NULL,再与现有的 CHECK 约束组合使用。

AUTO_INCREMENT

省略自动增长字段或显式写入 NULL,都会生成新编号:

START TRANSACTION;

INSERT INTO user_constraint_demo (id, name, age)
VALUES (NULL, '吴八', 30);

SELECT LAST_INSERT_ID() AS generated_id;

ROLLBACK;

即使事务回滚,已经申请的自动增长值也不保证被重新使用,因此后续编号出现空缺是正常现象。

在已有表上增删约束

下面先创建一张没有约束的空表,再使用 ALTER TABLE 添加字段属性和表级约束:

CREATE TABLE field_change_demo (
    id INT NOT NULL,
    code VARCHAR(20),
    score INT,
    status CHAR(1)
) ENGINE = InnoDB;

ALTER TABLE field_change_demo
    MODIFY COLUMN code VARCHAR(20) NOT NULL,
    ALTER COLUMN status SET DEFAULT '1',
    ADD PRIMARY KEY (id),
    ADD CONSTRAINT uk_field_change_code UNIQUE (code),
    ADD CONSTRAINT chk_field_change_score
        CHECK (score BETWEEN 0 AND 100);

ALTER TABLE field_change_demo
    MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT;

SHOW CREATE TABLE field_change_demo;

删除主键前要先移除依赖它的 AUTO_INCREMENT 属性。以下代码依次恢复字段定义并删除约束:

ALTER TABLE field_change_demo
    MODIFY COLUMN id INT NOT NULL;

ALTER TABLE field_change_demo
    MODIFY COLUMN code VARCHAR(20) NULL,
    ALTER COLUMN status DROP DEFAULT,
    DROP CHECK chk_field_change_score,
    DROP INDEX uk_field_change_code,
    DROP PRIMARY KEY;

DROP TABLE field_change_demo;

外键约束

创建子表时添加外键

下面创建一张临时演示用的员工表,并在建表语句中直接添加外键:

CREATE TABLE emp_create_fk_demo (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    dept_id INT,
    CONSTRAINT fk_emp_create_demo_dept
        FOREIGN KEY (dept_id)
        REFERENCES dept (id)
        ON DELETE SET NULL
        ON UPDATE CASCADE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

SHOW CREATE TABLE emp_create_fk_demo;

DROP TABLE emp_create_fk_demo;

dept_id 允许为 NULL,因此可以配合 ON DELETE SET NULL 使用。

为已有表添加外键

数据准备中的所有 emp.dept_id 都能在 dept.id 中找到对应值,因此可以直接添加外键:

ALTER TABLE emp
    ADD CONSTRAINT fk_emp_dept
    FOREIGN KEY (dept_id)
    REFERENCES dept (id);

SHOW CREATE TABLE emp;

这里省略了 ON DELETE 和 ON UPDATE,两者默认都是 NO ACTION。

验证子表引用

写入存在的部门编号可以成功;写入 NULL 也可以成功,因为 emp.dept_id 没有 NOT NULL 约束:

START TRANSACTION;

INSERT INTO emp (name, dept_id)
VALUES ('市场部测试员工', 2), ('待分配员工', NULL);

SELECT id, name, dept_id
FROM emp
WHERE name IN ('市场部测试员工', '待分配员工')
ORDER BY id;

ROLLBACK;

写入不存在的部门编号 99 会破坏引用完整性,下面的语句预期执行失败:

INSERT INTO emp (name, dept_id)
VALUES ('无效部门员工', 99);
NO ACTION 与 RESTRICT

研发部被多名员工引用。在默认的 NO ACTION 行为下,删除研发部会被立即拒绝:

-- 预期失败:研发部仍被员工引用
DELETE FROM dept
WHERE id = 1;

直接修改被引用的部门编号也会被拒绝:

-- 预期失败:被引用的部门编号不能直接修改
UPDATE dept
SET id = 10
WHERE id = 1;

把外键显式定义为 RESTRICT 后,执行这两条语句会得到相同结果。

市场部没有被准备数据中的员工引用,因此删除它不会破坏引用完整性:

START TRANSACTION;

DELETE FROM dept
WHERE id = 2;

SELECT id, name
FROM dept
ORDER BY id;

ROLLBACK;
删除外键与校验已有数据

删除外键后,数据库不再检查 emp.dept_id 与 dept.id 的关系:

ALTER TABLE emp
DROP FOREIGN KEY fk_emp_dept;

SHOW INDEX FROM emp;

INSERT INTO emp (name, dept_id)
VALUES ('无效部门员工', 99);

此时表中已经存在无效部门编号,直接重新添加外键会校验失败:

-- 预期失败:现有的 dept_id = 99 无法匹配父表
ALTER TABLE emp
    ADD CONSTRAINT fk_emp_dept
    FOREIGN KEY (dept_id)
    REFERENCES dept (id);

先清理无效数据,再重新添加约束:

DELETE FROM emp
WHERE name = '无效部门员工' AND dept_id = 99;

ALTER TABLE emp
    ADD CONSTRAINT fk_emp_dept
    FOREIGN KEY (dept_id)
    REFERENCES dept (id)
    ON DELETE RESTRICT
    ON UPDATE RESTRICT;
CASCADE

先把外键改为级联行为:

ALTER TABLE emp
DROP FOREIGN KEY fk_emp_dept;

ALTER TABLE emp
    ADD CONSTRAINT fk_emp_dept
    FOREIGN KEY (dept_id)
    REFERENCES dept (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE;

下面在事务中依次演示更新级联和删除级联,最后使用 ROLLBACK 恢复行数据和引用值:

START TRANSACTION;

-- 父表编号由 1 改为 10,匹配的 emp.dept_id 会同步改为 10
UPDATE dept
SET id = 10
WHERE id = 1;

SELECT id, name, dept_id
FROM emp
WHERE dept_id = 10
ORDER BY id;

-- 删除部门 10,dept_id = 10 的员工记录也会被删除
DELETE FROM dept
WHERE id = 10;

SELECT id, name, dept_id
FROM emp
ORDER BY id;

ROLLBACK;

ROLLBACK 不保证自动增长计数器回退,因此后续插入记录的编号可能仍然出现空缺;如果需要完全重置演示环境,可以重新执行数据准备。

SET NULL

先把外键改为 SET NULL,前提是 emp.dept_id 允许保存 NULL:

ALTER TABLE emp
DROP FOREIGN KEY fk_emp_dept;

ALTER TABLE emp
    ADD CONSTRAINT fk_emp_dept
    FOREIGN KEY (dept_id)
    REFERENCES dept (id)
    ON DELETE SET NULL
    ON UPDATE SET NULL;

更新父表被引用键时,匹配的子表外键会变为 NULL;删除父表记录时也是如此:

START TRANSACTION;

-- 原来属于研发部的员工,其 dept_id 会被设置为 NULL
UPDATE dept
SET id = 10
WHERE id = 1;

SELECT id, name, dept_id
FROM emp
WHERE id BETWEEN 2 AND 6
ORDER BY id;

-- 金庸原来属于总经办,删除总经办后其 dept_id 会被设置为 NULL
DELETE FROM dept
WHERE id = 5;

SELECT id, name, dept_id
FROM emp
WHERE id = 1;

ROLLBACK;
SET DEFAULT

SET DEFAULT 不能作为 InnoDB 的成功案例。下面使用独立演示表展示其语法,避免影响 emp 当前的外键:

CREATE TABLE set_default_demo (
    id INT PRIMARY KEY,
    dept_id INT DEFAULT 2,
    CONSTRAINT fk_set_default_demo_dept
        FOREIGN KEY (dept_id)
        REFERENCES dept (id)
        ON DELETE SET DEFAULT
        ON UPDATE SET DEFAULT
) ENGINE = InnoDB;

按照 MySQL 官方文档,InnoDB 应拒绝这个定义。部分 MySQL 8.0 版本可能允许建表并在 SHOW CREATE TABLE 中保留相关子句,但父表发生变化时仍不会执行 SET DEFAULT 语义,因此同样不能使用。

如果当前版本接受了建表语句,可以删除这张独立演示表:

DROP TABLE IF EXISTS set_default_demo;

演示结束后,把 emp 的外键从 SET NULL 恢复为默认行为:

ALTER TABLE emp
DROP FOREIGN KEY fk_emp_dept;

ALTER TABLE emp
    ADD CONSTRAINT fk_emp_dept
    FOREIGN KEY (dept_id)
    REFERENCES dept (id);