6.1 作用
限定某个表的某个字段的引用完整性。
比如:员工表的员工所在部门的选择,必须在部门表能找到对应的部分。
6.2 关键字
FOREIGN KEY
6.3 主表和从表/父表和子表
主表(父表):被引用的表,被参考的表
从表(子表):引用别人的表,参考别人的表
例如:员工表的员工所在部门这个字段的值要参考部门表:部门表是主表,员工表是从表。
例如:学生表、课程表、选课表:选课表的学生和课程要分别参考学生表和课程表,学生表和课程表是主表,选课表是从表。
6.4 特点
(1)从表的外键列,必须引用/参考主表的主键或唯一约束的列
为什么?因为被依赖/被参考的值必须是唯一的
(2)在创建外键约束时,如果不给外键约束命名,默认名不是列名,而是自动产生一个外键名(例如 student_ibfk_1;),也可以指定外键约束名。
(3)创建(CREATE)表时就指定外键约束的话,先创建主表,再创建从表
(4)删表时,先删从表(或先删除外键约束),再删除主表
(5)当主表的记录被从表参照时,主表的记录将不允许删除,如果要删除数据,需要先删除从表中依赖该记录的数据,然后才可以删除主表的数据
(6)在“从表”中指定外键约束,并且一个表可以建立多个外键约束
(7)从表的外键列与主表被参照的列名字可以不相同,但是数据类型必须一样,逻辑意义一致。如果类型不一样,创建子表时,就会出现错误“ERROR 1005 (HY000): Can’t create table’database.tablename’(errno: 150)”。
例如:都是表示部门编号,都是int类型。
(8)当创建外键约束时,系统默认会在所在的列上建立对应的普通索引。但是索引名是外键的约束名。(根据外键查询效率很高)
(9)删除外键约束后,必须手动删除对应的索引
6.5 添加外键约束
(1)建表时
create table 主表名称(字段1 数据类型 primary key,字段2 数据类型);create table 从表名称(字段1 数据类型 primary key,字段2 数据类型,[CONSTRAINT <外键约束名称>] FOREIGN KEY(从表的某个字段) references 主表名(被参考字段));#(从表的某个字段)的数据类型必须与主表名(被参考字段)的数据类型一致,逻辑意义也一样#(从表的某个字段)的字段名可以与主表名(被参考字段)的字段名一样,也可以不一样-- FOREIGN KEY: 在表级指定子表中的列-- REFERENCES: 标示在父表中的列
create table dept( #主表did int primary key, #部门编号dname varchar(50) #部门名称);create table emp(#从表eid int primary key, #员工编号ename varchar(5), #员工姓名deptid int, #员工所在的部门foreign key (deptid) references dept(did) #在从表中指定外键约束#emp表的deptid和和dept表的did的数据类型一致,意义都是表示部门的编号);说明:(1)主表dept必须先创建成功,然后才能创建emp表,指定外键成功。(2)删除表时,先删除从表emp,再删除主表dept
(2)建表后
一般情况下,表与表的关联都是提前设计好了的,因此,会在创建表的时候就把外键约束定义好。不过,如果需要修改表的设计(比如添加新的字段,增加新的关联关系),但没有预先定义外键约束,那么,就要用修改表的方式来补充定义。
格式:
ALTER TABLE 从表名 ADD [CONSTRAINT 约束名] FOREIGN KEY (从表的字段) REFERENCES 主表名(被引用字段) [on update xx][on delete xx];
举例:
ALTER TABLE emp1ADD [CONSTRAINT emp_dept_id_fk] FOREIGN KEY(dept_id) REFERENCES dept(dept_id);
举例:
create table dept(did int primary key, #部门编号dname varchar(50) #部门名称);create table emp(eid int primary key, #员工编号ename varchar(5), #员工姓名deptid int #员工所在的部门);#这两个表创建时,没有指定外键的话,那么创建顺序是随意
alter table emp add foreign key (deptid) references dept(did);
6.6 演示问题
(1)失败:不是键列
create table dept(did int , #部门编号dname varchar(50) #部门名称);create table emp(eid int primary key, #员工编号ename varchar(5), #员工姓名deptid int, #员工所在的部门foreign key (deptid) references dept(did));#ERROR 1215 (HY000): Cannot add foreign key constraint 原因是dept的did不是键列
(2)失败:数据类型不一致
create table dept(did int primary key, #部门编号dname varchar(50) #部门名称);create table emp(eid int primary key, #员工编号ename varchar(5), #员工姓名deptid char, #员工所在的部门foreign key (deptid) references dept(did));#ERROR 1215 (HY000): Cannot add foreign key constraint 原因是从表的deptid字段和主表的did字段的数据类型不一致,并且要它俩的逻辑意义一致
(3)成功,两个表字段名一样
create table dept(did int primary key, #部门编号dname varchar(50) #部门名称);create table emp(eid int primary key, #员工编号ename varchar(5), #员工姓名did int, #员工所在的部门foreign key (did) references dept(did)#emp表的deptid和和dept表的did的数据类型一致,意义都是表示部门的编号#是否重名没问题,因为两个did在不同的表中);
(4)添加、删除、修改问题
create table dept(did int primary key, #部门编号dname varchar(50) #部门名称);create table emp(eid int primary key, #员工编号ename varchar(5), #员工姓名deptid int, #员工所在的部门foreign key (deptid) references dept(did)#emp表的deptid和和dept表的did的数据类型一致,意义都是表示部门的编号);
insert into dept values(1001,'教学部');insert into dept values(1003, '财务部');insert into emp values(1,'张三',1001); #添加从表记录成功,在添加这条记录时,要求部门表有1001部门insert into emp values(2,'李四',1005);#添加从表记录失败ERROR 1452 (23000): Cannot add(添加) or update(修改) a child row: a foreign key constraint fails (`atguigudb`.`emp`, CONSTRAINT `emp_ibfk_1` FOREIGN KEY (`deptid`) REFERENCES `dept` (`did`)) 从表emp添加记录失败,因为主表dept没有1005部门
mysql> select * from dept;+------+--------+| did | dname |+------+--------+| 1001 | 教学部 || 1003 | 财务部 |+------+--------+2 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid |+-----+-------+--------+| 1 | 张三 | 1001 |+-----+-------+--------+1 row in set (0.00 sec)
update emp set deptid = 1002 where eid = 1;#修改从表失败ERROR 1452 (23000): Cannot add(添加) or update(修改) a child row(子表的记录): a foreign key constraint fails(外键约束失败) (`atguigudb`.`emp`, CONSTRAINT `emp_ibfk_1` FOREIGN KEY (`deptid`) REFERENCES `dept` (`did`)) #部门表did字段现在没有1002的值,所以员工表中不能修改员工所在部门deptid为1002update dept set did = 1002 where did = 1001;#修改主表失败ERROR 1451 (23000): Cannot delete(删除) or update(修改) a parent row(父表的记录): a foreign key constraint fails (`atguigudb`.`emp`, CONSTRAINT `emp_ibfk_1` FOREIGN KEY (`deptid`) REFERENCES `dept` (`did`)) #部门表did的1001字段已经被emp引用了,所以部门表的1001字段就不能修改了。update dept set did = 1002 where did = 1003;#修改主表成功 因为部门表的1003部门没有被emp表引用,所以可以修改
delete from dept where did=1001; #删除主表失败ERROR 1451 (23000): Cannot delete(删除) or update(修改) a parent row(父表记录): a foreign key constraint fails (`atguigudb`.`emp`, CONSTRAINT `emp_ibfk_1` FOREIGN KEY (`deptid`) REFERENCES `dept` (`did`)) #因为部门表did的1001字段已经被emp引用了,所以部门表的1001字段对应的记录就不能被删除
总结:约束关系是针对双方的
- 添加了外键约束后,主表的修改和删除数据受约束
- 添加了外键约束后,从表的添加和修改数据受约束
- 在从表上建立外键,要求主表必须存在
- 删除主表时,要求从表从表先删除,或将从表中外键引用该主表的关系先删除
6.7 约束等级
Cascade方式:在父表上update/delete记录时,同步update/delete掉子表的匹配记录Set null方式:在父表上update/delete记录时,将子表上匹配记录的列设为null,但是要注意子表的外键列不能为not nullNo action方式:如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作Restrict方式:同no action, 都是立即检查外键约束Set default方式(在可视化工具SQLyog中可能显示空白):父表有变更时,子表将外键列设置成一个默认的值,但Innodb不能识别
如果没有指定等级,就相当于Restrict方式。
对于外键约束,最好是采用: ON UPDATE CASCADE ON DELETE RESTRICT 的方式。
(1)演示1:on update cascade on delete set null
create table dept(did int primary key, #部门编号dname varchar(50) #部门名称);create table emp(eid int primary key, #员工编号ename varchar(5), #员工姓名deptid int, #员工所在的部门foreign key (deptid) references dept(did) on update cascade on delete set null#把修改操作设置为级联修改等级,把删除操作设置为set null等级);
insert into dept values(1001,'教学部');insert into dept values(1002, '财务部');insert into dept values(1003, '咨询部');insert into emp values(1,'张三',1001); #在添加这条记录时,要求部门表有1001部门insert into emp values(2,'李四',1001);insert into emp values(3,'王五',1002);
mysql> select * from dept;mysql> select * from emp;
#修改主表成功,从表也跟着修改,修改了主表被引用的字段1002为1004,从表的引用字段就跟着修改为1004了mysql> update dept set did = 1004 where did = 1002;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from dept;+------+--------+| did | dname |+------+--------+| 1001 | 教学部 || 1003 | 咨询部 || 1004 | 财务部 | #原来是1002,修改为1004+------+--------+3 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid |+-----+-------+--------+| 1 | 张三 | 1001 || 2 | 李四 | 1001 || 3 | 王五 | 1004 | #原来是1002,跟着修改为1004+-----+-------+--------+3 rows in set (0.00 sec)
#删除主表的记录成功,从表对应的字段的值被修改为nullmysql> delete from dept where did = 1001;Query OK, 1 row affected (0.01 sec)mysql> select * from dept;+------+--------+| did | dname | #记录1001部门被删除了+------+--------+| 1003 | 咨询部 || 1004 | 财务部 |+------+--------+2 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid |+-----+-------+--------+| 1 | 张三 | NULL | #原来引用1001部门的员工,deptid字段变为null| 2 | 李四 | NULL || 3 | 王五 | 1004 |+-----+-------+--------+3 rows in set (0.00 sec)
(2)演示2:on update set null on delete cascade
create table dept(did int primary key, #部门编号dname varchar(50) #部门名称);create table emp(eid int primary key, #员工编号ename varchar(5), #员工姓名deptid int, #员工所在的部门foreign key (deptid) references dept(did) on update set null on delete cascade#把修改操作设置为set null等级,把删除操作设置为级联删除等级);
insert into dept values(1001,'教学部');insert into dept values(1002, '财务部');insert into dept values(1003, '咨询部');insert into emp values(1,'张三',1001); #在添加这条记录时,要求部门表有1001部门insert into emp values(2,'李四',1001);insert into emp values(3,'王五',1002);
mysql> select * from dept;+------+--------+| did | dname |+------+--------+| 1001 | 教学部 || 1002 | 财务部 || 1003 | 咨询部 |+------+--------+3 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid |+-----+-------+--------+| 1 | 张三 | 1001 || 2 | 李四 | 1001 || 3 | 王五 | 1002 |+-----+-------+--------+3 rows in set (0.00 sec)
#修改主表,从表对应的字段设置为nullmysql> update dept set did = 1004 where did = 1002;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from dept;+------+--------+| did | dname |+------+--------+| 1001 | 教学部 || 1003 | 咨询部 || 1004 | 财务部 | #原来did是1002+------+--------+3 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid |+-----+-------+--------+| 1 | 张三 | 1001 || 2 | 李四 | 1001 || 3 | 王五 | NULL | #原来deptid是1002,因为部门表1002被修改了,1002没有对应的了,就设置为null+-----+-------+--------+3 rows in set (0.00 sec)
#删除主表的记录成功,主表的1001行被删除了,从表相应的记录也被删除了mysql> delete from dept where did=1001;Query OK, 1 row affected (0.00 sec)mysql> select * from dept;+------+--------+| did | dname | #部门表中1001部门被删除+------+--------+| 1003 | 咨询部 || 1004 | 财务部 |+------+--------+2 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid |#原来1001部门的员工也被删除了+-----+-------+--------+| 3 | 王五 | NULL |+-----+-------+--------+1 row in set (0.00 sec)
(3)演示:on update cascade on delete cascade
create table dept(did int primary key, #部门编号dname varchar(50) #部门名称);create table emp(eid int primary key, #员工编号ename varchar(5), #员工姓名deptid int, #员工所在的部门foreign key (deptid) references dept(did) on update cascade on delete cascade#把修改操作设置为级联修改等级,把删除操作也设置为级联删除等级);
insert into dept values(1001,'教学部');insert into dept values(1002, '财务部');insert into dept values(1003, '咨询部');insert into emp values(1,'张三',1001); #在添加这条记录时,要求部门表有1001部门insert into emp values(2,'李四',1001);insert into emp values(3,'王五',1002);
mysql> select * from dept;+------+--------+| did | dname |+------+--------+| 1001 | 教学部 || 1002 | 财务部 || 1003 | 咨询部 |+------+--------+3 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid |+-----+-------+--------+| 1 | 张三 | 1001 || 2 | 李四 | 1001 || 3 | 王五 | 1002 |+-----+-------+--------+3 rows in set (0.00 sec)
#修改主表,从表对应的字段自动修改mysql> update dept set did = 1004 where did = 1002;Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> select * from dept;+------+--------+| did | dname |+------+--------+| 1001 | 教学部 || 1003 | 咨询部 || 1004 | 财务部 | #部门1002修改为1004+------+--------+3 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid |+-----+-------+--------+| 1 | 张三 | 1001 || 2 | 李四 | 1001 || 3 | 王五 | 1004 | #级联修改+-----+-------+--------+3 rows in set (0.00 sec)
#删除主表的记录成功,主表的1001行被删除了,从表相应的记录也被删除了mysql> delete from dept where did=1001;Query OK, 1 row affected (0.00 sec)mysql> select * from dept;+------+--------+| did | dname | #1001部门被删除了+------+--------+| 1003 | 咨询部 || 1004 | 财务部 |+------+--------+2 rows in set (0.00 sec)mysql> select * from emp;+-----+-------+--------+| eid | ename | deptid | #1001部门的员工也被删除了+-----+-------+--------+| 3 | 王五 | 1004 |+-----+-------+--------+1 row in set (0.00 sec)
6.8 删除外键约束
流程如下:
(1)第一步先查看约束名和删除外键约束SELECT * FROM information_schema.table_constraints WHERE table_name = '表名称';#查看某个表的约束名ALTER TABLE 从表名 DROP FOREIGN KEY 外键约束名;(2)第二步查看索引名和删除索引。(注意,只能手动删除)SHOW INDEX FROM 表名称; #查看某个表的索引名ALTER TABLE 从表名 DROP INDEX 索引名;
举例:
mysql> SELECT * FROM information_schema.table_constraints WHERE table_name = 'emp';mysql> alter table emp drop foreign key emp_ibfk_1;Query OK, 0 rows affected (0.02 sec)Records: 0 Duplicates: 0 Warnings: 0
mysql> show index from emp;mysql> alter table emp drop index deptid;Query OK, 0 rows affected (0.01 sec)Records: 0 Duplicates: 0 Warnings: 0mysql> show index from emp;
6.9 开发场景
问题1:如果两个表之间有关系(一对一、一对多),比如:员工表和部门表(一对多),它们之间是否一定要建外键约束?
答:不是的
问题2:建和不建外键约束有什么区别?
答:建外键约束,你的操作(创建表、删除表、添加、修改、删除)会受到限制,从语法层面受到限制。例如:在员工表中不可能添加一个员工信息,它的部门的值在部门表中找不到。
不建外键约束,你的操作(创建表、删除表、添加、修改、删除)不受限制,要保证数据的引用完整性,只能依靠程序员的自觉,或者是在Java程序中进行限定。例如:在员工表中,可以添加一个员工的信息,它的部门指定为一个完全不存在的部门。
问题3:那么建和不建外键约束和查询有没有关系?
答:没有
在 MySQL 里,外键约束是有成本的,需要消耗系统资源。对于大并发的 SQL 操作,有可能会不适合。比如大型网站的中央数据库,可能会
因为外键约束的系统开销而变得非常慢。所以, MySQL 允许你不使用系统自带的外键约束,在应用层面完成检查数据一致性的逻辑。也就是说,即使你不用外键约束,也要想办法通过应用层面的附加逻辑,来实现外键约束的功能,确保数据的一致性。
6.10 阿里开发规范
【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
