15.1.20.5 外键约束
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
symbol
值,则使用该值。 -
如果没有定义
CONSTRAINT
symbol
子句,或者在CONSTRAINT
关键字后不包括符号,则自动生成约束名称。如果没有定义
CONSTRAINT
symbol
子句,或者在CONSTRAINT
关键字后不包括符号,BothInnoDB
和NDB
存储引擎忽略FOREIGN_KEY
。index_name
-
如果定义了
CONSTRAINT
值,它必须在数据库中唯一。重复的symbol
symbol
将导致类似于:ERROR 1005 (HY000):无法创建表'test.fk1'(errno:121)的错误。 -
NDB 集群使用创建时的字母大小写存储外键名称。
FOREIGN KEY ... REFERENCES子句中的表和列标识符可以在反引号(`
)中引用。或者,如果启用了ANSI_QUOTES
SQL 模式,可以使用双引号("
)。系统变量lower_case_table_names
也将被考虑。
外键约束受以下条件和限制:
-
父表和子表必须使用相同的存储引擎,并且不能定义为临时表。
-
创建外键约束需要在父表上拥有
REFERENCES
特权。 -
外键和引用键对应的列必须具有相似的数据类型。固定精度类型,如
INTEGER
和DECIMAL
的大小和符号必须相同。字符串类型的长度不需要相同。对于非二进制字符列,字符集和排序规则必须相同。 -
MySQL 支持表内的一对一外键引用。在这些情况下,一张“子表记录”指的是同一张表中的依赖记录。
-
MySQL 需要在外键和引用键上创建索引,以便快速执行外键检查,不需要扫描整个表。在引用表中,必须存在一个索引,其中的外键列顺序与列名相同。如果不存在,这个索引将自动创建。这个索引可能会在你创建另一个可以用于约束外键的索引时被悄然删除。
index_name
,如果给出,遵循之前的描述。 -
InnoDB
允许外键引用任何索引列或一组列。然而,在被引用表中,必须存在一个索引,其中被引用列的顺序与当前顺序相同。隐藏的索引列,InnoDB
添加到索引中的也被考虑(见第17.6.2.1节,“聚簇和次级索引”)。NDB
需要在被引用列上显式指定唯一键(或主键)。InnoDB
不需要,这是标准SQL的扩展。 -
InnoDB
目前不支持用户定义分区的表中的外键引用,这包括父表和子表。这个限制不适用于
NDB
表,如果这些表是通过KEY
或LINEAR KEY
(NDB
存储引擎支持的唯一用户分区类型)进行分区;这些表可以拥有外键引用或是外键引用目标。 -
外键关系中的表不能被更改以使用另一个存储引擎。要更改存储引擎,必须首先删除任何外键约束。
-
外键约束不能引用虚拟生成列。
关于 MySQL 外键约束实现与 SQL 标准的差异,请参阅第 1.7.2.3 节,“FOREIGN KEY Constraint Differences”。
当UPDATE
或DELETE
操作影响父表中的键值,该值在子表中具有匹配行,结果取决于引用操作,由ON UPDATE
和ON DELETE
子句指定的FOREIGN KEY
子句中。引用操作包括:
-
CASCADE
: 删除或更新父表中的行,并自动删除或更新子表中的匹配行。支持ON DELETE CASCADE
和ON UPDATE CASCADE
。在两个表之间,不要定义多个UPDATE
子句,作用于父表或子表中的同一列。如果在外键关系中定义了两个表的
FOREIGN KEY
子句,使得两个表都成为父表和子表,那么必须为另一个FOREIGN KEY
子句定义ON UPDATE CASCADE
或ON DELETE CASCADE
子句,以便级联操作成功。如果只对一个FOREIGN KEY
子句定义了ON UPDATE CASCADE
或ON DELETE CASCADE
子句,级联操作将失败。Note级联外键操作不激活触发器。
-
SET NULL
:从父表中删除或更新行,并将子表中的外键列或列设置为NULL
。支持ON DELETE SET NULL
和ON UPDATE SET NULL
子句。如果指定了
SET NULL
操作,请确保在子表中没有声明该列为NOT NULL
。 -
RESTRICT
:拒绝父表的删除或更新操作。指定RESTRICT
(或NO ACTION
)与省略ON DELETE
或ON UPDATE
子句相同。 -
NO ACTION
: 标准SQL中的关键字。在InnoDB
中,这等同于RESTRICT
;如果父表中有与之相关的外键值,删除或更新操作将被立即拒绝。NDB
支持延迟检查,并且NO ACTION
指定延迟检查;在使用时,约束检查直到提交时间。注意,对于NDB
表,这导致父和子表的所有外键检查都被延迟。 -
SET DEFAULT
: MySQL解析器识别这个动作,但是InnoDB
和NDB
都拒绝包含ON DELETE SET DEFAULT
或ON UPDATE SET DEFAULT
子句的表定义。
对于支持外键的存储引擎,MySQL拒绝任何INSERT
或UPDATE
操作,如果子表中没有与父表相匹配的候选键值。
对于未指定的ON DELETE
或ON UPDATE
,默认动作总是NO ACTION
。
作为默认值,明确指定的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 在dump文件中正确地定义了表的结构,包括子表的外键约束,以便在 reload dump 文件时更方便地重新加载包含外键关系的表。为了使dump文件中的表可以任意顺序重新加载,mysqldump 自动在dump输出中包含一个禁用
foreign_key_checks
语句。这样可以避免由于dump文件中的表顺序不正确而导致的外键检查问题。禁用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
您可以从信息架构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 Server中的Error 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
子句中指定的用户,而不是调用该程序的用户。如果该用户拥有父表的表级权限,仍然会显示父表信息。在这种情况下,存储程序创建者需要隐藏信息,包括适当的条件处理器。