该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
多张表的数据库设计:拓展你的表
到了某个时候,一张表不够了:
我们在上一章中就已经体会到,对于四季饮品的表,查询饮品信息还是销售信息,都只能从这一张表查询;
当数据变得越来越复杂,这唯一的一张表就显得不够用了;表里充满了多余的数据,浪费空间又会拖慢速度;
本章我们将学习用多张表来记录数据、控制数据;
新建示例数据库及表:
新建我的饮品店中关于饮品的数据库及表:hq_drink_list、drink_list;
并插入若干条数据;
不具有原子性的burdening(配料)列:
如果我们查询包含banana的饮品,我们将不得不使用LIKE搭配通配符的方式:%banana%;
如果继续限定一些其他的查询条件,这个查询的并不能算是简单;
再如果,想查询配料包含apple的饮品信息呢。。。就要写好多类似的查询,当表变得庞大之后,查询会很麻烦;
我们的表应该是为了节省精力而设计的,别为了克服设计不良的表而执意改善查询;
创建多个列来存储各项配料:
可见各个配料放在一列中存储是很不方便的查询;
我们需要重新设计他的表;
添加更多配料列:
只有一个配料列会使查询非常不精确;
我们已经学会使用ALTER修改表,拆分文本字符串到多个列;
我们可以把各个配料存入不同的列中,嗯,4个列应该足够了;
一切从头开始:
我们使用burdening1、burdening2、burdening3、burdening4表示各个配料;
但是,如此这般,我们的查询配料包含banana的条件很可能是这样的:
burdening1 = ‘banana’ OR burdening2 = ‘banana’ ORburdening3 = ‘banana’ OR burdening4 = ‘banana’;
当查询包含apple和banana的记录时,还需要用AND链接两个上边的子句,显然这是很糟的方式;
一切都失败了:
我们想的拆分多个列,并没能让查询更加简单;而且修改之后的表也不符合原子性的需求;
跳出一张表的思考框框:
在目前的这一张表中不会有什么好的方案了;
我们需要更多能与现有内容合作的表,让每种饮品和他们的配料之间产生关联;
我们需要把不符合原子性的列移入新的表;
我们现在的表:
我们要把原本的一张表扩大成更有效果的一组相关的表:(图示:Final)
现在让我们一步一步的学习,这些都是如何实现的;
数据库模式:
模式(schema)用于表达数据库内的结构,包括表和列,以及他们之间相互连接的方式;
创建数据库的视觉解析图,在设计查询时有助于理解数据相连的方式;模式也能用文字表达;
模式:
对数据库内的数据描述(列和表),以及任何相关对象和各种连接方式的描述就称为SCHEMA,即模式;
类似上边图所示的图表,我们需要知道的是:为表创建图表可以协助我们分别看待表的设计与其中的数据;
如何从一张表变成两张表:
我们知道配料列的多个值,或者多个配料列都会使查询非常难写;
我们先解决原子性的问题:使用另外一张表来存储配料;
步骤:
1)移出配料列并把它存储至专属表:
每项配料为一行;
2)添加足以识别 drink_list表中每项饮品的配料 的列:
在新的表中,我们需要识别各项配料分别属于哪项饮品;
我们需要吧drink_list表中的某些列加入新表来建立连接;
图表中加入的连接:
连接表中相符列使用箭头或线段;因为使用标准的模式schema图示符号,任何SQL开发者都可以理解;
如上图中drink_info和burdening表之间的箭头连线;
现在我们的查询语句可以是:(模式参照上边图)
先从drink_info中查询出符合条件的饮品的唯一记录标识,这里可以是id,并且id是主键,使用主键连接表是很好的方式;(如果建立连接的是原表中多个列的组合,那这里需要查出来的就是符合条件的多个列的值,要求的是这里的多个列可以唯一标识原表中的一条记录)
使用查出来的id作为条件,查询出相应的配料;
这个虽然看起来复杂了,但是却呈现出如何以一张表的内容来提取另外一张表的内容;
这样burdenings表中的配料搭配唯一的drink_info的主键id,对应原drink_list表中的唯一一行;
连接你的表:
我们需要使用独一无二的列来连接一切;
还好我们已经开始规范化了,在drink_info中确实有独一无二的列——主键;
把drink_info.id主键作为burdenings表的一列;
利用这个外来的主键列 我们能知道某个配料属于哪个饮品——这种方式成为外键(foreign key);
外键:
外键是表中的某一列,他引用到另一个表的主键;
外键可能与它引用的主键名称不同;
外键使用的主键也被称为父键(parent key);
该主键所在的表又被称为父表(parent table);
外键能用于确认一张表中的行与另外一张表中的行相对应;
外键的值可以是NULL,即使主键值不可谓NULL;
外键值不需要唯一——事实上,外键通常都没有唯一性;
外键为NULL,表示在父表中没有相符的主键;但我们可以确保外键包含有意义、已存储在父表中的值——通过约束(constraint)来实现;
外键约束:
外键只能用CREATE或ALTER语句来指定;
创建在结构内的键被称为约束(constraint);
引用完整性:
referential integrity;
插入外键列的值必须已经存在于父表的来源列中;
你可以使用外键来引用父表中某个唯一的值;
外键不一定必须是父表的主键,但必须有唯一性;
创建/更新表时加入外键:
CREATE和ALTER TABLE时都可以加入外键;
我们尝试创建burdenings表,创建带有约束的外键drink_id,父表为drink_info;
相关SQL:
mysql> CREATE TABLE drink_info
-> (
-> id INT NOT NULL AUTO_INCREMENT,
-> drink_name VARCHAR(30) NOT NULL,
-> cost DEC(4,2) NOT NULL DEFAULT 20.00,
-> meter INT NOT NULL DEFAULT 200,
-> drink_des VARCHAR(100),
-> PRIMARY KEY(id)
-> );
Query OK, 0 rows affected (0.09 sec)
mysql> CREATE TABLE burdenings
-> (
-> id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
-> name VARCHAR(30),
-> drink_id INT NOT NULL,
-> CONSTRAINT drink_info_id_fk
-> FOREIGN KEY(drink_id)
-> REFERENCES drink_info(id)
-> );
Query OK, 0 rows affected (0.09 sec)
分析:
1)创建外键和创建索引列一样:把外键列设定为INT与NOT NULL;
2)直接在主键后加入PRIMARY KEY是指定主键的另一种方式;
3)CONSTRAINT约束:指明来源表和键的名称,同时还说明它是一个外键(fk);
4)FOREIGN KEY指明外键列;
5)REFERENCES参考:指明外键来源,以及外键列在另外一张表中的名称;
MUL表示这一列可以存储多个相同的值,他也是追踪每个drink_id使用哪些配料的关键;
创建外键约束的优点:
创建外键约束后,就只能插入存在于父表中的值,有助于加强两张表间的连接;
外键约束能确保引用完整性,即约束能确保通过外键与另一张表中的某一行对应;
如果我们试图删除主键表中的行或修改主键值,而这个主键是其他表的外键约束时,就会收到警告;
要想删除的话,需要先移除外键行;
约束的种类:
主键约束;我们现在看见的外键约束;创建列时使用关键字也被视为一种约束;
还有一种检查约束CHECK,同于指定某个条件,列必须满足条件之后才能插入新值;(请自行查阅)
new day:
我们来回顾下之前所做的,然后继续学习;
现在我们已经知道了可以将表拆分成多个表,并建立表之间连接,来优化我们的数据库结构和查询,这个过程有一定的模式可以遵循;
我们上边的连接数据库表的模式并不一定是最好的;接下来让我们详细介绍下数据库的几种模式;
数据库模式(schema):
1.数据模式:一对一
两个表之间的连接,对应两个表的记录都只有一条;
如某位员工的信息表和员工的薪资单表就是一对一的关系,因为一个人只能有一个属于自己的工资表;
在模式图中,一对一的连接线是单纯的实线,表示连接一件事物与另一件事物;
在“图示Final”中:
drink_info和drink_des表之间的连接就是一对一的关系;
一种饮品对应唯一的描述,一条描述信息对应唯一的饮品;
使用一对一的时机:
我们并不会经常使用一对一的表;
通常,把一对一的数据留在主表更合理,但有时候也适合把某些列拉出来;
1)抽出数据或许能让你写出更快速的查询,比如你想知道你的工资单,到相应的表中查询是最好的方式;
2)如果有列包含还不知道的值,可以单独存储这一列,以免主要表中出现NULL;
3)希望某些数据不要太常被 访问;隔离这些数据即可管制访问次数;以员工表为例,他们的薪资信息最好存为另一张表;
4)如果有一大块数据,例如BLOB类型,这段数据或许存为另一张表会更好;
一对一:父表只有一行与子表的某行相关;
2.数据模式:一对多
表A中的某条记录在表B中可以对应多条记录;
但表B中的每一条记录都只会对应到A表中的某一条记录;
在“图示Final”中:
表drink_info的burdening_main列与burdenings表之间的连线就是一对多的关系;
一条饮品只能有一种主配料,但是一种配料可以是多种饮品的主配料;
3.数据模式:多对多
在“图示Final”中:
drink_info和burdening_list 以及 drink_info和sale_list表之间都是多对多的关系,我们以前者为例进行说明;
一个饮品对应多种配料,一种配料也可以用在多种饮品中;
一对一的连接线是线段;
一对多的连接线是带箭头的线段;
多对多的连接线是两侧都带箭头的线段;
4.数据模式:我们需要junction table
对于多对多的关系,无论在哪个表中添加外键,都会造成表中出现重复的数据;
我们一开始创建的带有外键的burdenings表,由于是多对多的关系,就会有这个问题,同一个配料会出现好多次;
连接表(junction table):
直接连接两张表最后都会出现重复的数据;
因此,我们需要在两个多对多的表之间架设一个中间桥梁,来存储所有的drink_id和burdening_id;
这就是所谓的连接表junction table,用来存储两个相关表的主键;
对应的“图示:Final”中的burdening_list表就是连接表;
我们单独再画一个图示如下:
分析这张图:
可以看出,我们通过junction table将多对多的关系转化成来能个一对多的关系;
即many-many -> one-many and one-many;
多对多:用junction table存储原始表的主键;
5.数据模式:再看多对多
多对多的秘密:通常由两个一对多关系并利用junction table在中间连接构成;
遇到多对多,一定要用中间表;避免造成重复组;
改成此方式的优点会在下一章的联接(join)中有直观的体现;
Junction table有助于保持数据完整性:如必须删除某一饮品,这种连接会让我们无需分心注意配料表,只需管理中间表;
其次,由于避免了重复数据,在更新数据的时候,也会更加顺利;
数据模式:修正篇-规范化的更多级别:
1.第一范式,1NF:
规则一:数据列只包含具有原子性的值;
规则二:没有重复的数据组(含义交叉的数据列);
为了满足1NF,我们创建了带有外键的burdenings表(由于多对多的关系,会有重复的记录),如果不考虑主键id,其他两个列一起使用就能形成唯一的主键,唯一标识符这条记录;
1)组合键:
由两列以上组合的键称为组合键;
是由多个数据列构成的主键,组合个列后形成具有唯一性的键;
组合键体现出了一种关系,即表中的数据列本身对其他列也有关系;
2)函数依赖:
functional dependency;
当某列的数据必须随着另一列的数据的改变而改变的时候,表示第一列函数依赖于第二列;
比如,对于姓名以及姓名的缩写两列数据,姓名的缩写就 函数依赖于 姓名,因为缩写会随着姓名的变化而变化;
这种依赖性在有些列之间比较常见;
3)速记符号:
快速表示函数依赖的方式:T.x -> T.y,即在关系表T中,y列函数依赖于x列;(从右读到左就是解读依赖性的方式)
4)完全不依赖:
比如人的信息表中,除了姓名和缩写外,还存储了其恋人所在的城市;
恋人可以随时换一座城市,但这并不会影像表中其他的信息,因此表示恋人city的这一列是个完全不依赖的列;
5)部分函数依赖性:
partial functional dependency;
指非主键列依赖于组合主键的某一部分,但又不完全依赖于组合主键;
6)传递函数依赖性:
这是对于非键列的相互关联的一种思考;
如果改变任何非键列可能造成其他列的改变,即为传递依赖;
传递函数依赖表现了任何非键列与另一个非键列有关联;
使用人造主键id可以有效避免部分函数依赖性的问题,这类id是新的专用于索引的字段,就表示没有字段依赖它;
在junction table中使用自然键(组合键)还是人造键(id)是个值得商榷的问题,本书主要采用单一人造主键,用于维持语法简单;
2.第二范式,2NF:
在表中新增主键列有助于达成2NF;因为第二范式重点是 表的主键如何与其他数据产生关系;
只要所有列都是主键的一部分或者表中有唯一主键列符合1NF的表 也会符合2NF;后者也是指定一个AUTO_INCREMENT的ID列的好理由;
规则一:符合1NF;
规则二:没有部分函数依赖性;
3.第三范式,3NF:
由于加上了人工主键(artificial primary key)符合2NF并不困难,任何具有人工主键且没有主键的表都符合2NF;
规则一:符合2NF;
规则二:没有传递函数依赖性;
传递函数依赖性表示的是任何非键列与相同表中其他非键列有联系;
如果改变任何非键列可能造成其他任何非键列的改变,就是具有传递函数依赖性;
类似之前的例子,一个人的信息中存储了 它恋人的一系列信息,这一系列信息之间就很大可能存在传递函数依赖性;
最终的修改:
现在再来看我们的原始表:
以及,我们修改之后的模式图:
仔细思考下,看看我们新的数据库结构是否经得住:数据库模式、数据库规范化:1NF、2NF、3NF的考验;
总结:
数据库模式是描述 表与表之间关系的;
数据库规范是描述 列与列之间关系的;
表之间通过外键建立连接,添加相应的外键约束;
1.Scheme:数据库模式,描述数据库中的数据、其他相关对象,以及这些对象相互连接的方式;
2.One-toOne relationship:一对一关系,父表中的一行记录至于子表中的一行记录相关联;
3.One-to_Many relationship:一对多关系,一张表中的一行记录可能与另一张表中的多行相关联,但最后一张表中的任一行记录只会与前一张表中的一行记录相关联;
4.Many-to-Many relationship:多对多关系,两个通过junction table连接的表,让一张表中的多行记录能与另一张表中的多行记录相关联,反之亦然;
5.First Normal Form(1NF):第一范式,列中只包含原子性数据,而且列内没有重复的数据组;
6.Transitive functional dependency:传递函数依赖,指任何非键列依赖于另一个非键列;
7.Second normal form(2NF):第二范式,先符合1NF,同时不能包含部分函数依赖性;
8.Third normal form(3NF):第三范式,先符合2NF,同时不可包含可传递函数依赖;
9.Foreign key:外键,引用其他表主键的列;
10.Composite key:组合键,由多个列构成的主键,这些列需形成唯一的键值;
值得注意的是,本章并没有介绍更多的SQL语句,在下一章,我们会看到,使用数据库模式和遵循数据库规范会给我们的查询带来哪些好处。