MySQL 支持外键,它们允许跨表引用相关数据,并且外键约束有助于保持相关数据的一致性。
外键关系涉及到一个父表,它持有初始列值,以及一个子表,其中的列值引用父表的列值。外键约束是在子表上定义的。
在 CREATE TABLE
或 ALTER TABLE
语句中定义外键约束的基本语法包括以下内容:
[CONSTRAINT [symbol]] FOREIGN KEY
[index_name] (col_name, ...)
REFERENCES tbl_name (col_name,...)
[ON DELETE reference_option]
[ON UPDATE reference_option]
reference_option:
RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT
本节中还将讨论以下主题:
外键约束命名遵循以下规则:
-
如果定义了
CONSTRAINT
符号
值,则使用该值。 -
如果未定义
CONSTRAINT
符号
子句,或者在CONSTRAINT
关键字后未包含符号,则自动生成约束名称。如果不定义
CONSTRAINT
符号
子句,或者在CONSTRAINT
关键字后没有符号,bothInnoDB
和NDB
存储引擎将忽略FOREIGN_KEY
。索引名称
-
如果定义了
CONSTRAINT
值,该值必须在数据库中是唯一的。重复的符号
符号
将导致类似于:错误 1005 (HY000): 无法创建表 'test.fk1' (errno: 121)。 -
NDB 集群使用与创建时相同的字母大小写来存储外键名称。
在 FOREIGN KEY ... REFERENCES
子句中,表和列标识符可以用反引号 (`
) 引用。或者,如果启用了 ANSI_QUOTES
SQL 模式,可以使用双引号 ("
)。还将考虑 lower_case_table_names
系统变量设置。
外键约束受以下限制和约束:
-
父表和子表必须使用相同的存储引擎,并且不能定义为临时表。
-
创建外键约束需要在父表上拥有
REFERENCES
权限。 -
外键和引用的键的对应列必须具有相似的数据类型。固定精度类型,如
INTEGER
和DECIMAL
,必须具有相同的大小和符号。字符串类型的长度无需相同。对于非二进制(字符)字符串列,字符集和排序规则必须相同。 -
MySQL 支持在同一表中的一个列和另一个列之间的外键引用。(一列不能对自己具有外键引用。)在这些情况下,一个 “子表记录” 指的是同一表中的一个依赖记录。
-
MySQL 需要在外键和引用键上创建索引,以便快速执行外键检查,而不需要扫描整个表。在引用表中,必须在外键列上创建索引,这些列必须是索引的第一列,以相同的顺序。如果不存在这样的索引,MySQL 将自动创建一个。如果您创建了另一个可以用来执行外键约束的索引,可能会悄悄地删除该索引。
index_name
,如果给定,将按照之前描述的方式使用。 -
InnoDB
允许外键引用任何索引列或列组。但是,在被引用表中,必须在引用列上创建索引,这些列必须是索引的第一列,以相同的顺序。InnoDB
添加到索引中的隐藏列也被考虑在内(见 第 17.6.2.1 节,“聚簇索引和辅助索引”)。NDB
需要在任何作为外键引用的列上创建明确的唯一键(或主键)。InnoDB
不需要,这是标准 SQL 的扩展。 -
InnoDB
目前不支持用户定义的分区表的外键。这包括父表和子表。这条限制不适用于
NDB
表,这些表可以通过KEY
或LINEAR KEY
进行分区(NDB
存储引擎支持的唯一用户定义的分区类型);这些表可以具有外键引用或是外键引用的目标。 -
在外键关系中,不能将表更改为使用另一个存储引擎。要更改存储引擎,必须首先删除任何外键约束。
-
外键约束不能引用虚拟生成的列。
有关 MySQL 实现外键约束与 SQL 标准的差异,请参阅 第 1.6.2.3 节,“FOREIGN KEY 约束差异”。
当UPDATE
或DELETE
操作影响父表中的键值,并且在子表中有匹配的行时,结果取决于参照操作由ON UPDATE
和ON DELETE
子句指定的FOREIGN KEY子句。参照操作包括:
-
CASCADE
:从父表中删除或更新行,并自动删除或更新子表中的匹配行。同时支持ON DELETE CASCADE
和ON UPDATE CASCADE
。在两个表之间,不要定义多个ON UPDATE CASCADE
子句,作用于父表或子表中的同一列。如果在foreign key关系中的两个表上都定义了FOREIGN KEY子句,使得两个表都是父表和子表,那么在一个FOREIGN KEY子句中定义的
ON UPDATE CASCADE
或ON DELETE CASCADE
子句必须在另一个FOREIGN KEY子句中定义,以便级联操作成功。如果只有一个FOREIGN KEY子句定义了ON UPDATE CASCADE
或ON DELETE CASCADE
子句,级联操作将失败并报错。Note级联foreign key操作不会激活触发器。
-
SET NULL
:从父表中删除或更新行,并将子表中的foreign key列或列设置为NULL
。同时支持ON DELETE SET NULL
和ON UPDATE SET NULL
子句。如果您指定了
SET NULL
操作,请确保您没有在子表中将列声明为NOT NULL
。 -
RESTRICT
:拒绝父表的删除或更新操作。指定RESTRICT
(或NO ACTION
)与省略ON DELETE
或ON UPDATE
子句相同。 -
不采取行动
: 来自标准SQL的关键字。对于InnoDB
,这等同于RESTRICT
;父表的删除或更新操作将立即被拒绝,如果在引用的表中存在相关的外键值。NDB
支持延迟检查,不采取行动
指定了延迟检查;当使用时,约束检查将在提交时执行。注意,对于NDB
表,所有外键检查都将被延迟执行。 -
设置默认值
: MySQL解析器识别这个动作,但是InnoDB
和NDB
都拒绝包含ON DELETE SET DEFAULT
或ON UPDATE SET DEFAULT
子句的表定义。
对于支持外键的存储引擎,MySQL将拒绝任何INSERT
或UPDATE
操作,该操作尝试在子表中创建外键值,而在父表中没有匹配的候选键值。
对于未指定的ON DELETE
或ON UPDATE
,默认操作始终是不采取行动
。
默认情况下,明确指定的ON DELETE NO ACTION
或ON UPDATE NO ACTION
子句不会出现在SHOW CREATE TABLE
输出中,也不会出现在使用mysqldump导出的表中。RESTRICT
,这是一个等效的非默认关键字,将出现在SHOW CREATE TABLE
输出中,也将出现在使用mysqldump导出的表中。
对于NDB
表,ON UPDATE CASCADE
不支持父表的主键引用。
对于 NDB
表,ON DELETE CASCADE
不支持在子表包含一个或多个 TEXT
或 BLOB
类型的列时。(Bug #89511, Bug #27484882)
InnoDB
使用深度优先搜索算法在对应于外键约束的索引的记录上执行级联操作。
存储生成列上的外键约束不能使用 CASCADE
、SET NULL
或 SET DEFAULT
作为 ON UPDATE
引用操作,也不能使用 SET NULL
或 SET DEFAULT
作为 ON DELETE
引用操作。
存储生成列的基列上的外键约束不能使用 CASCADE
、SET NULL
或 SET DEFAULT
作为 ON UPDATE
或 ON DELETE
引用操作。
这个简单的示例将 parent
和 child
表通过单列外键关联:
CREATE TABLE parent (
id INT NOT NULL,
PRIMARY KEY (id)
) ENGINE=INNODB;
CREATE TABLE child (
id INT,
parent_id INT,
INDEX par_ind (parent_id),
FOREIGN KEY (parent_id)
REFERENCES parent(id)
ON DELETE CASCADE
) ENGINE=INNODB;
这是一个更复杂的示例,其中 product_order
表有两个其他表的外键。一个外键引用 product
表中的双列索引,另一个引用 customer
表中的单列索引:
CREATE TABLE product (
category INT NOT NULL, id INT NOT NULL,
price DECIMAL,
PRIMARY KEY(category, id)
) ENGINE=INNODB;
CREATE TABLE customer (
id INT NOT NULL,
PRIMARY KEY (id)
) ENGINE=INNODB;
CREATE TABLE product_order (
no INT NOT NULL AUTO_INCREMENT,
product_category INT NOT NULL,
product_id INT NOT NULL,
customer_id INT NOT NULL,
PRIMARY KEY(no),
INDEX (product_category, product_id),
INDEX (customer_id),
FOREIGN KEY (product_category, product_id)
REFERENCES product(category, id)
ON UPDATE CASCADE ON DELETE RESTRICT,
FOREIGN KEY (customer_id)
REFERENCES customer(id)
) ENGINE=INNODB;
您可以使用以下 ALTER TABLE
语法将外键约束添加到现有表:
ALTER TABLE tbl_name
ADD [CONSTRAINT [symbol]] FOREIGN KEY
[index_name] (col_name, ...)
REFERENCES tbl_name (col_name,...)
[ON DELETE reference_option]
[ON UPDATE reference_option]
外键可以是自我参照的(引用同一表)。当您使用 ALTER TABLE
将外键约束添加到表时,请先在外键引用的列上创建索引。
您可以使用以下 ALTER TABLE
语法删除外键约束:
ALTER TABLE tbl_name DROP FOREIGN KEY fk_symbol;
如果在创建约束时 FOREIGN KEY
子句定义了 CONSTRAINT
名称,则可以使用该名称删除外键约束。否则,约束名称将被内部生成,您必须使用该值。要确定外键约束名称,请使用 SHOW CREATE TABLE
:
mysql> SHOW CREATE TABLE child\G
*************************** 1. row ***************************
Table: child
Create Table: CREATE TABLE `child` (
`id` int DEFAULT NULL,
`parent_id` int DEFAULT NULL,
KEY `par_ind` (`parent_id`),
CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`)
REFERENCES `parent` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
mysql> ALTER TABLE child DROP FOREIGN KEY `child_ibfk_1`;
在同一个 ALTER TABLE
语句中添加和删除外键是支持的 ALTER TABLE ... ALGORITHM=INPLACE
。它不支持 ALTER TABLE ... ALGORITHM=COPY
。
在 MySQL 中,InnoDB 和 NDB 表支持外键约束检查。外键检查由 foreign_key_checks
变量控制,默认情况下启用。通常,在正常操作期间启用该变量以强制参照完整性。foreign_key_checks
变量对 NDB
表的效果与对 InnoDB
表的效果相同。
foreign_key_checks
变量是动态的,支持全局和会话范围。有关使用系统变量的信息,请参阅 第 7.1.9 节,“使用系统变量”。
禁用外键检查是有用的情况:
-
删除被外键约束引用的表。只有在
foreign_key_checks
禁用后,才能删除该表。删除表时,定义在该表上的约束也将被删除。 -
在不同的顺序中重新加载表,而不是按照外键关系所需的顺序。例如,mysqldump 在转储文件中生成正确的表定义,包括子表的外键约束。为了使重新加载转储文件变得更容易,mysqldump 自动在转储输出中包含一条语句,以禁用
foreign_key_checks
。这使您可以在转储文件中以任何顺序导入表,即使转储文件包含未正确排序的表以满足外键关系。禁用foreign_key_checks
也可以加速导入操作,避免外键检查。 -
执行
LOAD DATA
操作,以避免外键检查。 -
对具有外键关系的表执行
ALTER TABLE
操作。
当foreign_key_checks
被禁用时,外键约束将被忽略,以下情况除外:
-
重新创建一个之前删除的表将返回错误,如果表定义不符合引用该表的外键约束。该表必须具有正确的列名和类型。它还必须在引用的键上具有索引。如果这些要求不满足,MySQL 将返回错误 1005,该错误消息将引用 errno: 150,这意味着外键约束未正确形成。
-
更改表将返回错误(errno: 150),如果更改后的表的外键定义不正确。
-
删除外键约束所需的索引。必须在删除索引之前删除外键约束。
-
创建一个外键约束,其中一个列引用了不匹配的列类型。
禁用foreign_key_checks
还有以下影响:
-
允许删除包含外键的数据库,这些外键被数据库外的表引用。
-
允许删除具有外键的表,这些外键被其他表引用。
-
启用
foreign_key_checks
不会触发表数据的扫描,这意味着在foreign_key_checks
禁用时添加到表中的行在重新启用foreign_key_checks
时不会被检查一致性。
MySQL 扩展元数据锁,以便在相关表上执行必要的锁定。扩展元数据锁可以防止在相关表上同时执行冲突的 DML 和 DDL 操作。此功能还允许在父表修改时更新外键元数据。在早期的 MySQL 版本中,外键元数据,由子表所有,无法安全地更新。
如果表被显式锁定使用 LOCK TABLES
,则相关表将被隐式打开和锁定。对于外键检查,将在相关表上获取共享只读锁 (LOCK TABLES READ
)。对于级联更新,将在相关表上获取共享-nothing 写锁 (LOCK TABLES WRITE
)。
要查看外键定义,请使用 SHOW CREATE TABLE
:
mysql> SHOW CREATE TABLE child\G
*************************** 1. row ***************************
Table: child
Create Table: CREATE TABLE `child` (
`id` int DEFAULT NULL,
`parent_id` int DEFAULT NULL,
KEY `par_ind` (`parent_id`),
CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`)
REFERENCES `parent` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
您可以从 Information Schema KEY_COLUMN_USAGE
表中获取外键信息。下面是一个查询示例:
mysql> SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE REFERENCED_TABLE_SCHEMA IS NOT NULL;
+--------------+------------+-------------+-----------------+
| TABLE_SCHEMA | TABLE_NAME | COLUMN_NAME | CONSTRAINT_NAME |
+--------------+------------+-------------+-----------------+
| test | child | parent_id | child_ibfk_1 |
+--------------+------------+-------------+-----------------+
您可以从 INNODB_FOREIGN
和 INNODB_FOREIGN_COLS
表中获取特定于 InnoDB 外键的信息。下面是一个查询示例:
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FOREIGN \G
*************************** 1. row ***************************
ID: test/child_ibfk_1
FOR_NAME: test/child
REF_NAME: test/parent
N_COLS: 1
TYPE: 1
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FOREIGN_COLS \G
*************************** 1. row ***************************
ID: test/child_ibfk_1
FOR_COL_NAME: parent_id
REF_COL_NAME: id
POS: 0
在涉及 InnoDB
表的外键错误事件中(通常是 MySQL 服务器中的错误 150),可以通过检查 SHOW ENGINE INNODB STATUS
输出来获取最新的外键错误信息。
mysql> SHOW ENGINE INNODB STATUS\G
...
------------------------
LATEST FOREIGN KEY ERROR
------------------------
2018-04-12 14:57:24 0x7f97a9c91700 Transaction:
TRANSACTION 7717, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3
MySQL thread id 8, OS thread handle 140289365317376, query id 14 localhost root update
INSERT INTO child VALUES (NULL, 1), (NULL, 2), (NULL, 3), (NULL, 4), (NULL, 5), (NULL, 6)
Foreign key constraint fails for table `test`.`child`:
,
CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`) ON DELETE
CASCADE ON UPDATE CASCADE
Trying to add in child table, in index par_ind tuple:
DATA TUPLE: 2 fields;
0: len 4; hex 80000003; asc ;;
1: len 4; hex 80000003; asc ;;
But in parent table `test`.`parent`, in index PRIMARY,
the closest match we can find is record:
PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 000000001e19; asc ;;
2: len 7; hex 81000001110137; asc 7;;
...
如果用户拥有所有父表的表级权限,ER_NO_REFERENCED_ROW_2
和 ER_ROW_IS_REFERENCED_2
错误消息将暴露父表信息。如果用户没有所有父表的表级权限,将显示更通用的错误消息(ER_NO_REFERENCED_ROW
和 ER_ROW_IS_REFERENCED
)。
一个例外是,对于使用 DEFINER
权限执行的存储程序,评估权限的用户是程序 DEFINER
子句中的用户,而不是调用用户。如果该用户拥有父表的表级权限,仍将显示父表信息。在这种情况下,存储程序的创建者有责任通过包含适当的条件处理程序来隐藏信息。