文章详情

短信预约信息系统项目管理师 报名、考试、查分时间动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

第05期:外键到底能不能用?

2015-09-09 15:30

关注

第05期:外键到底能不能用?

这篇主要说明表属性 - 外键。

外键的设计初衷是为了在数据库端保证对逻辑上相关联的表数据在操作上的一致性与完整性。

外键在大部分企业写的开发规范里会**直接规避掉!**外键有优缺点,也并不是说每种场景都不适用,完全没有必要一刀切。外键到底能不能用?下面会针对不同的场景来告诉你答案。

一、外键的优缺点

优点:

缺点:

二、外键的使用

外键参照动作列表:

那先来简单看看 MySQL 里外键的用法。MySQL 外键仅有 InnoDB 和 NDB 两种引擎支持,这里只关注 InnoDB。

本次示例 MySQL 的版本为最新版 8.0.19

示例

下面 f1 是父表,f2、f3、f6 分别代表不同类型的外键表,也就是子表。

-- 引用基础表,也就是父表
mysql-(ytt_fk/3305)->create table f1(id int primary key, 
	r1 int, r2 int, r3 int,key idx_r1(r1),key idx_u1 (r2,r3));
Query OK, 0 rows affected (0.02 sec)

--   随着参照表级联更新外键表,也就是父表更新的话,会级联更新子表的外键
mysql-(ytt_fk/3305)->create table f2(id int primary key, 
	f1_r1 int, mark int, constraint f1_fk_r1 foreign key (f1_r1) references f1(r1) on update cascade);
Query OK, 0 rows affected (0.02 sec)


--  随着参照表更新外键值为 NULL,也就是父表更新的话,会级联更新子表的外键为 NULL
mysql-(ytt_fk/3305)->create table f3 (id int primary key, 
	f1_id int, foreign key (f1_id) references f1(id) on update set null);
Query OK, 0 rows affected (0.02 sec)

--  多个键值外键。子表的可以引用父表非主键的其他键
mysql-(ytt_fk/3305)->create table f6 ( id int auto_increment primary key, 
	f1_r2 int, f1_r3 int, foreign key (f1_r2,f1_r3) references f1(r2,r3));
Query OK, 0 rows affected (0.02 sec)
场景一:强烈要求数据一致性,程序弱化,数据库端强化,表结构改动小,并发不高的场景。

用一条记录验证表 f2 和 f6。从功能性角度来看,外键的优势很明显,在数据库端完全满足了数据完整性校验。

mysql-(ytt_fk/3305)->insert into f1 values (1,10,100,1000);
Query OK, 1 row affected (0.00 sec)

mysql-(ytt_fk/3305)->insert into f2 values (1,1);
Query OK, 1 row affected (0.01 sec)

mysql-(ytt_fk/3305)->insert into f6 values (1,100,1000);
Query OK, 1 row affected (0.00 sec)

-- 更新引用表 f1
mysql-(ytt_fk/3305)->update f1 set id = 2 where id =1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

-- f2 也成功级联更新
mysql-(ytt_fk/3305)->select * from f2;
+----+-------+
| id | f1_id |
+----+-------+
|  1 |     2 |
+----+-------+
1 row in set (0.00 sec)

-- 引用表 r2 字段不允许更新,因为表 f6 有针对字段 r2 的外键约束。
mysql-(ytt_fk/3305)->update f1 set r2 = 11 ;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`ytt_fk`.`f6`, CONSTRAINT `f6_ibfk_1` FOREIGN KEY (`f1_r2`, `f1_r3`) REFERENCES `f1` (`r2`, `r3`))
场景二:频繁的数据装载,但是也严格要求数据库端保证数据一致性。

这里只验证表 f6,同时克隆一张新表 f6_no_fk ,除了没有外键,表结构和 f6 一样。导入 400W 条样例数据。

-- 导入 f6,有外键,时间 32 秒多。
mysql-(ytt_fk/3305)->load data infile "/var/lib/mysql-files/f1_sub.dat" into table f6;
Query OK, 4000000 rows affected (32.57 sec)
Records: 4000000  Deleted: 0  Skipped: 0  Warnings: 0

-- 导入 f6_no_fk,没有外键,时间 25 秒多。
mysql-(ytt_fk/3305)->load data infile "/var/lib/mysql-files/f1_sub.dat" into table f6_no_fk;
Query OK, 4000000 rows affected (25.95 sec)
Records: 4000000  Deleted: 0  Skipped: 0  Warnings: 0

从上面看到,单独的测试导入 400W 条记录,带有外键的表比非外键的表时间上没有优势。那针对上面的场景优化下,关闭外键检查参数,导入完成后,再开启。

mysql-(ytt_fk/3305)->truncate f6;
Query OK, 0 rows affected (0.04 sec)

-- 关闭外键检查。
mysql-(ytt_fk/3305)->set foreign_key_checks=0;
Query OK, 0 rows affected (0.00 sec)

-- 重新导入,时间28秒多。
mysql-(ytt_fk/3305)->load data infile "/var/lib/mysql-files/f1_sub.dat" into table f6;
Query OK, 4000000 rows affected (28.42 sec)
Records: 4000000  Deleted: 0  Skipped: 0  Warnings: 0

-- 开启外键检查。
mysql-(ytt_fk/3305)->set foreign_key_checks=1;
Query OK, 0 rows affected (0.00 sec)

从以上结果看出,关闭外键检查后,导入时间和没有外键的表 f6_no_fk 差不多。

场景三:并发少,事物块简单。

接下来再看下简单的事物块提交方式,我简单写了一个每 500 条记录提交一次的存储过程。

DELIMITER $$
CREATE DEFINER=`ytt`@`127.0.0.1` PROCEDURE `sp_generate_data`(IN `tb_name` VARCHAR(64), IN `f_number` INT)
begin
declare i int default 0;
set @@autocommit=0;
while i < f_number DO

  set @stmt = concat("insert into ",tb_name,"(f1_r2,f1_r3) values (ceil(rand()*10),ceil(rand()*10))");
  prepare s1 from @stmt;
  execute s1;
  set i = i + 1;
  if mod(i,500)=0 THEN
    commit;
  end if;
end while;
drop prepare s1;
commit;
set @@autocommit=1;
end$$
DELIMITER ;

接下来插入 100W 条记录,

-- 外键表写入总时间为 1 分 14 秒
mysql> call sp_generate_data("f6",1000000);
Query OK, 0 rows affected (1 min 14.14 sec)

-- 非外键表写入时间为 1 分 8 秒
mysql> call sp_generate_data("f6_no_fk",1000000);
Query OK, 0 rows affected (1 min 8.45 sec)

-- 关闭外键检查
mysql> set foreign_key_checks=0;
Query OK, 0 rows affected (0.00 sec)

-- 时间为 1 分 4 秒
mysql> call sp_generate_data("f6",1000000);
Query OK, 0 rows affected (1 min 4.28 sec)

mysql> set foreign_key_checks=1;
Query OK, 0 rows affected (0.00 sec)

从测试的结果来看,有外键和没有外键的检索时间在这样的场景下也相差无几。

场景四:主表的外键引用字段类型要扩充,原来的数据溢出,没法保存更大的值。

比如此时字段 r2 定义的数据类型不合适了,需要更改为大点的,比如以下,直接修改会报错,

mysql-(ytt_fk/3305)->alter table f1 change r2 r2 bigint;
ERROR 3780 (HY000): Referencing column "f1_r2" and referenced column "r2" in foreign key constraint "f6_ibfk_1" are incompatible.

mysql-(ytt_fk/3305)->alter table f6 change f1_r2 f1_r2 bigint;
ERROR 3780 (HY000): Referencing column "f1_r2" and referenced column "r2" in foreign key constraint "f6_ibfk_1" are incompatible.

那怎么改呢?需要先把外键删掉,修改完了类型,再加上约束。这种场景就不太适合用外键。

mysql-(ytt_fk/3305)->alter table f6 drop constraint f6_ibfk_1;
Query OK, 0 rows affected (0.00 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql-(ytt_fk/3305)->alter table f6 change f1_r2 f1_r2 bigint;
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql-(ytt_fk/3305)->alter table f1 change r2 r2 bigint;
Query OK, 100000 rows affected (0.73 sec)
Records: 100000  Duplicates: 0  Warnings: 0

mysql-(ytt_fk/3305)->alter table f6 add foreign key (f1_r2,f1_r3) references f1(r2,r3);
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0
场景五:子表有触发器需求来更新必要的字段。

那关于这点就是,子表的触发器不会随着父表的更新级联应用,也就是此时触发器失效。举个例子,往 f2 上添加一个 before update 触发器。

-- 前置更新触发器
CREATE TRIGGER `tr_af_update` BEFORE UPDATE ON `f2`
 FOR EACH ROW set new.mark = new.f1_r1;

mysql-(ytt_fk/3305)->insert into f2 values (1,10,5);
Query OK, 1 row affected (0.00 sec)

mysql-(ytt_fk/3305)->select * from f2;
+----+-------+------+
| id | f1_r1 | mark |
+----+-------+------+
|  1 |    10 |    5 |
+----+-------+------+
1 row in set (0.00 sec)

-- 更新父表,

mysql-(ytt_fk/3305)->update f1 set r1 = 2 where r1 = 10;
Query OK, 5133 rows affected (0.15 sec)
Rows matched: 5133  Changed: 5133  Warnings: 0

-- 子表 f2对应的级联做了更改,但是触发器动作没执行。
mysql-(ytt_fk/3305)->select * from f2;
+----+-------+------+
| id | f1_r1 | mark |
+----+-------+------+
|  1 |     2 |    5 |
+----+-------+------+
1 row in set (0.00 sec)

-- 正常的操作应该这样
mysql-(ytt_fk/3305)->update f2 set id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

-- mark字段对应的克隆成了f1_r1字段的值。
mysql-(ytt_fk/3305)->select * from f2;
+----+-------+------+
| id | f1_r1 | mark |
+----+-------+------+
|  2 |     2 |    2 |
+----+-------+------+
1 row in set (0.00 sec)
场景六:父表为分区表,有外键的需求。

那针对分区表,暂时不支持子表以分区表为父表的外键。

mysql-(ytt_fk/3305)->create table f1_partition like f1;
Query OK, 0 rows affected (0.02 sec)

mysql-(ytt_fk/3305)->alter table f1_partition  partition by key() partitions 4;
Query OK, 0 rows affected (0.10 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql-(ytt_fk/3305)->create table f7 ( id int primary key, 
	f1_partition_id int, foreign key (f1_partition_id) references f1_partition(id));
ERROR 1506 (HY000): Foreign keys are not yet supported in conjunction with partitioning
场景七:日常并发很高的场景,应该尽量减少相关事务锁的范围和量级。

那举个简单例子,看看有外键情况下,父表更新,子表级联加锁的情形。

-- SESSION 1
mysql-(ytt_fk/3305)->begin;
Query OK, 0 rows affected (0.00 sec)

mysql-(ytt_fk/3305)->update f1 set r2 = 101 where r2 = 100;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0


mysql-(ytt_fk/3305)->select sys.ps_thread_id(connection_id()) as cid;
+------+
| cid  |
+------+
|   47 |
+------+
1 row in set (0.00 sec)

总共有 11 个锁,也就简单的执行了下 Update,而且更新的只是一行。

-- SESSION 2 
mysql-((none)/3305)->select count(*) from performance_schema.data_locks where thread_id = 47;
+----------+
| count(*) |
+----------+
|       11 |
+----------+
1 row in set (0.00 sec)

查看锁的细化,父有 f1 有 5 个锁,子表 f6 有 6 个锁。

这都是 MySQL 为了保证数据一致性强制加的,这点在 TPS 要求比较高的场景肯定不合适

mysql-((none)/3305)->select object_name,lock_type,lock_mode,lock_status,lock_data from performance_schema.data_locks where thread_id = 47 order by object_name;
+-------------+-----------+---------------+-------------+------------------------+
| object_name | lock_type | lock_mode     | lock_status | lock_data              |
+-------------+-----------+---------------+-------------+------------------------+
| f1          | TABLE     | IX            | GRANTED     | NULL                   |
| f1          | RECORD    | X             | GRANTED     | supremum pseudo-record |
| f1          | RECORD    | X             | GRANTED     | 100, 100, 1            |
| f1          | RECORD    | X,REC_NOT_GAP | GRANTED     | 1                      |
| f1          | RECORD    | X,GAP         | GRANTED     | 101, 100, 1            |
| f6          | TABLE     | IS            | GRANTED     | NULL                   |
| f6          | RECORD    | S,REC_NOT_GAP | GRANTED     | 100, 100, 12           |
| f6          | TABLE     | IX            | GRANTED     | NULL                   |
| f6          | RECORD    | X,REC_NOT_GAP | GRANTED     | 12                     |
| f6          | RECORD    | X,REC_NOT_GAP | GRANTED     | 101, 100, 12           |
| f6          | RECORD    | S,GAP         | GRANTED     | 101, 100, 12           |
+-------------+-----------+---------------+-------------+------------------------+
11 rows in set (0.00 sec)

三、外键的限制:

  1. 仅有 InnoDB 和 NDB 引擎支持。
  2. 不支持虚拟列。
  3. 不支持临时表。
  4. 外键列以及引用列数据类型、字符集、校对规则都得一致。
  5. 外键列以及引用列都必须建立索引。
  6. 外键引用多个列的,列顺序必须一致。
  7. 大对象字段不能作为引用列。
  8. constraint 命名必须在单个 database 里唯一。
  9. 外键级联更新操作不会触发子表上的触发器。
  10. 不支持分区表。

总结

本文主要从几个例子来演示了外键是否应该使用以及在哪些场景下使用,让大家了解外键的详细需求。

从上面我描述的几个场景来说,场景 1,2,3 很适合用外键;场景 4,5,6,7 就不太适合用外键;可以把外键功能放在数据库之外实现。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-数据库
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯