※食用指南:文章内容为‘CodeWithMosh’SQL进阶教程系列学习笔记,笔记整理比较粗糙,主要目的自存为主,记录完整的学习过程。(图片超级多,慎看!)
【中字】SQL进阶教程 | 史上最易懂SQL教程!10小时零基础成长SQL大师!!https://www.bilibili.com/video/BV1UE41147KC/?spm_id_from=333.1007.0.0&vd_source=b287f1f4a1fa54cc438e31a0f87ef4e2
目录:
第十二章:设计数据库——PART2
9、NORMALIZATION——标准化
10、FIRST NORMAL FORM(1NF)——第一范式
11、LINK TABLES——链接表
12、SECOND NORMAL FORM(2NF)——第二范式
13、THIRD NORMAL FORM(3NF)——第三范式
14、MY PRAGMATIC ADVICE——实用建议
15、DON’T MODEL THE UNIVERSE——不要对什么都建模
16、FORWARD ENGINEERING A MODEL——模型的正向工程
17、SYNCHRONIZING A MODEL——数据库同步模型
18、REVERSE ENGINEERING A DATABASE——模型的逆向工程
第十二章:设计数据库——PART2
9、NORMALIZATION——标准化
标准化:审查我们的设计,确保它遵循一些防止数据重复的预定义规则的这一过程
总共有七条范式(七范式),对于几乎99%的应用场景,只需要应用前三条范式
确保设计是最优的,不允许存在冗余或重复的数据
因为冗余增加了数据库的大小,使插入、更新和删除操作复杂化
如果某个名字在许多不同的地方重复,决定改名就需要更新好几个不同的地方,会造成无效报告
10、FIRST NORMAL FORM(1NF)——第一范式
第一范式:要求一行中的每个单元格都应该有单一值且不能出现重复列
courses中tags违反了第一范式
我们会在列中存储多个标签,并用逗号分隔它们,所以这个单元格或属性中会存在多个值
❗如果使用标签1、标签2、标签3这样的多个列是否可以
我们无法提前知道每门课有多少个标签,如果将来在课程中添加标签4的话,就必须返回修改设计,也不能保证这种情况不会再次发生
这种方法不具有扩展性
解决方法:需要把标签列从这张表中拎出来,并将其以一个名为tags的单独表建模,然后为tags和courses添加多对多关系
添加新表
设置属性:取决于数据库中有多少标签
需要在业务时搞清楚的问题,这些标签是由终端用户还是管理员创建
如果是终端用户,可能会迅速增加
先假设是由管理员创建并且只有50-60个标签——TINYINT(微整数型)
11、LINK TABLES——链接表
需要在课程和标签表之间添加“多对多“关系,但关系型数据库中并没有”多对多“关系,只有一对一、一对多关系
实现多对多,需要引入一张新的表——链接表(会有两段一对多关系,和注册表一样)
逻辑上,学生和课程之间是一种多对多的关系
链接表在关系型数据库中是很常见的
①建立新表
②把课程表的tags删掉
现在数据库就满足了第一范式
首先没有标签1、标签2、标签3这样的重复列,在一列中也没有多个值
所有标签都存在了一个地方,如果想重命名标签,只需要更新标签表中的一条记录
而在之前设计中,标签作为字符串重复了,在课程表中,每个标签都出现好多次,这样前端会在很多不同地方重复出现
如果想为前端改名,就必须更新好几条课程记录,每次进行更新或删除操作时,MySQL会锁定一行或多行,如果套用之前的设计,行会白白被锁定
想重命名一个标签,却要锁定一门课程,不合理
想要重命名一个标签,标签行应该是唯一需要锁定的行
12、SECOND NORMAL FORM(2NF)——第二范式
第二范式:一组关系必须符合第一范式,并且它不能有任何取决于这组关系任何候选键的任何真子集的非主属性
要求每张表都应该有一个单一目的:只能代表一种且仅有一种实体类型,而那种表中的每一列都应该用来描述那个实体
课程表有course_id、name、price这些列,所以他目的单一:存储课程记录
此表中的每一列都是课程的一种属性
❗如果有一列叫“注册日期“怎么办
注册日期:不是课程属性或课程性质
因为一门课可以容纳多名学生,每名学生可能会有不同的注册日期
所以这列不属于这张表,它不是一门课的属性,而是注册的属性,这就是引入注册实体并在其中添加了日期属性的原因。
简言之,第二范式:一张表中的每一列都应该在描述该表代表的实体
如果有一列描述了其他的东西,应该拿掉它,并放入一张单独表
🔺其他例子:
在这张orders表中,有order_id、date、customer_name,这不符合第二范式
这张表的目的是存储订单,order_id、date是订单属性,属于这张表,描述了一张订单;customer_name描述了一名顾客,而不是订单
在这种设计下,如果一名顾客订购了多笔订单,名字就会重复出现,会产生两个问题:
①浪费空间
②更新辛苦(如果想更新一个客户名,就必须在好几个地方更改它)
因此customer_name不属于orders表
应该拿出来,放到customers表中
现在顾客名被储存在了一个地方,没有浪费空间,如果想更改名字,只需要更新一条记录即可
🔺有时候不是上来就很清晰
如果一开始该列是customer,而不是customer_name怎么办
错误理解:每个订单都有一个顾客,所以它确实是订单的一种属性,所以这一列属于这张表
在这种设计下,顾客名会重复,只要出现这个现象,意味着设计没有标准化
能够单独存储但不会重复生成顾客名的地方——顾客表
需要一张单独表来存储所有顾客,在根据订单表中的customer_id引用它们
这也是不要更新主键的原因
表中有重复的customer_id,但那是重复数量最少的情况了,重复了4个字节长的整数,而不是50或者100字符长的顾客名
🔺回到案例:
这里的instructor不属于这张表
同一讲师教多门课,名字也会重复,也必须在好几个地方更改
①新建instructors表
②建立关系,并删除原instructor列
③设置好外键(如果一名讲师有授课,就无法删除那名讲师)
④通过顶部排列菜单整理模型
13、THIRD NORMAL FORM(3NF)——第三范式
第三范式:实体或者表首先应该符合第二范式,表中的所有属性只能由那组关系的候选键决定,而不能是任何非主属性
这张invoices表中有三列:invoice_id、payment_total、balance
❗是否可以通过invoice_id-payment_total得到balance
Balance取决于invoice_id、payment_total,如果这两个列的值变了,就必须重新计算
如果改变其中一个值,但忘了更新balance,数据就不一致了
这条记录中并不能了解实际balance是多少,该选择相信balance的值,还是用invoice_id-payment_total
应该删掉balance列,我们不需要它
第三范式:表中的列不应派生自其他列
🔺另一个例子
full_name不应该出现在这,因为它违反了第三范式
应该通过结合名字和姓氏得到它
第三范式和其他范式一样:减少重复,提高数据完整性
14、MY PRAGMATIC ADVICE——实用建议
不要担心记住这些标准化规则:第一条或者第二条规则,区别在哪(除非考试,需要回答这类问题),这些问题在现实世界中没有应用机会
当和别人合作实际项目时,只需要专注于消除冗余就可以了,不需要对每张表和每列逐个应用标准化规则,无人关心给定表是第二还是第三范式
就如看到这些重复值,且这些值不像是1、2、3、4这样的外键时
意味着设计没有标准化,至于违反了第几范式并不重要,无人关心,重点在如何消除重复
🔺①例子:
顾客表中有3列,customer_id、name、shipping_address
如果有要求,要支持多个收货地址,设计就会出问题
对于一位给定顾客,必须重复他们的名字来添加多个收货地址
如果还有两列(电邮、生日),为这名顾客存储多个收货地址,就必须要重复所有值,如何处理
①首先思考逻辑实体和这里的关系
顾客和地址是一对多的关系
一名顾客可以有很多收货地址
一个收货地址仅针对一名顾客
②逻辑模型转为实体模型,得到一下两张表
没有考虑任何标准化规则以及如何通过到处移动列来应用它们,只考虑逻辑实体和它们之间的关系
所以如果遵循同样的过程,不必顾虑标准化规则
先从逻辑和概念模型开始,不要直接跳到创建表
再次强调:业务需求
在这个例子中,假设需要支持多个收货地址的需求,基于这个要求,最初的设计违反了第三范式
但如果没有这个要求,这样的设计是完全合理的,所以不要盲目套用标准化规则,永远要把需求考虑进去
15、DON’T MODEL THE UNIVERSE——不要对什么都建模
许多数据建模人员犯的错:总是视图泛化模型,以便支持未来的需求
大多数时候,未来的需求只存在于脑中,实际不会发生
最终创建了一个过于复杂且没用的模型告终
(无视项目范围,无视项目背景,无视业务需求,对所有大大小小的事件都建模,最终超大的模型且除他以外无人能懂)
“这个模型可以处理所有未来需求,不需要任何更改“(试图一劳永逸)
有三张表(EAV):Entities、Attributes、Values
所有事物都是实体,都放Entities(实体表)
所有东西都有属性和值,存储在Attributes(属性表)、Values(值表)
本质是在关系型数据库上又建立了一个关系型数据库——第六范式
虽然理论上确实有点优势,现实中制造了一个没人能收拾的巨大混乱
如果想选择一条学生记录,首先要从三个表拿到数据,然后在分组数据,过于浪费时间和空间
只需要为现下问题指定最佳解决方案就可以,而不是可能永远不会发生的未来问题
🔺现实世界中
人们可能改法定名字,那就不要在顾客表中增加“过去的名字”,除非这对你的问题域真的很重要
人们会搬家,如果你只需知道顾客的当前住址,就不要想着支持多个地址
不要什么都要建模,或者太过参照现实世界建模
为你的问题域建模就好了,建立在当下的需求之上,而不是复杂的现实世界
别想着万一发生这个、发生那个,万一没有发生呢,最后得到一个负担很重还得拖拽很长一段时间的设计,就跑不快,在应用中也很难增添新特色
一切从简,简单才是最高境界
不要为设计了一个复杂的模型骄傲
拿到复杂模型并不断简化到其他人都做不到的程度才是真正值得骄傲的
这样就有了一个漂亮的模型,能以最佳的方式支持当下的需求,代码漂亮又不复杂,查询执行很快,所有人都能看懂这个设计,也能为了支持未来需求很轻松地拓展模型
永远无法预测所有未来需求,因为这取决于业务、取决于CEO的想法,而不是程序员的想法
未来有变,也总可以写查询修改数据模型,并在必要时迁移数据
16、FORWARD ENGINEERING A MODEL——模型的正向工程
目前建立的还只是一个模型,不是数据库
如何把模型转化为真实的物理数据库
①在数据库菜单中打开正向工程
②选择我们的连接
③保留所有默认设置不变
默认情况下连接的应该是MySQL工作台主页
如果密码没存,可能还需要输入密码
④设置这个导向要生成的脚本的选项
多数时候保持设置不变就好,除非知道自己在做什么
⑤选择要编写脚本的对象类型
目前为止,只在实体模型中创建了表,但也可以创建视图、存储历程(如存储过程和函数)、触发器和用户对象,但现在先不用考虑其他这些对象
⑥选择显示过滤器,从而编写对象中排除一张或多张表
默认情况下,所有的表都会被选中,可以排除一张或多张表,只用选中并移动到排除对象列表中,也可把它移回要处理的对象列表中
⑦这就是这个向导为创建数据库生成的脚本了(学校数据库、所有表)
暂时不用纠结语句细节
⑧保存为文件:可以保存为脚本文件,并录入源代码控制资源库
复制粘贴后返回MySQL工作台,粘贴到查询窗口中
⑨点击NEXT,向导会生成这个数据库
⑩回到会话,刷新导航面板
17、SYNCHRONIZING A MODEL——数据库同步模型
❗已经建立好数据库,想做任何更改怎么办,想添加一张新表或修改一张现有的表
在设计模式下打开这些表,然后可以做任何必要的更改(添加一列、修改、删除、重命名)
限于你是唯一使用这个数据库的人,可以用这个方法,没有其他人会使用
但在大中型组织架构中,通常有好几个服务器,模拟了实际生产环境
生产环境:用户访问我们的应用程序或数据库的地方
阶段环境:非常接近生产环境
测试环境:仅仅为测试用
开发环境
每个环境都有一个或多个服务器
所以任何时候开发人员想要对这些数据库做任何更改,需要能够在其他数据库上复制相同的更改,在不同环境里这些数据库应该具有一致性
❗如何办到
①回到模型,做出更改然后将这个模型与数据库同步
在enrollments表中添加一个新列——优惠券
假设学生在注册时要使用的优惠券
为了保持一致使用VARCHAR(50)属性;设置非空值(不是每个人都能在注册时使用优惠券)
②随后选择同步模型
在没有数据的时候使用正向工程,也就是用一个模型生成一个数据库
有一个数据库想,想将数据库与模型同步选择同步模型
③选择连接到本地计算机的一个数据库
如果我是组织的开发人员,还可以访问测试环境、阶段环境、生产环境
所以此出不选择本地实例连接,选择连接到一个测试环境的数据库
④保留所有默认设置不动
⑤MySQL工作台检测到我们正在使用学校这个数据库
⑥检测到了需要更新的表
因为更新了注册表,会有一个绿色箭头
注册表和课程表有相关联,课程表可能也会受影响,课程和课程标签之间也是
因为两个表有关系,MySQL工作台不得不暂时取消这两个表之间的关系,以便在注册表中应用必要更改,然后再次启用
⑦数据库代码自动更新
可以看到一堆ALTER TABLE、DROP FOREIGN KEY语句,它们用于消除表之间的关系⑧
在下面可以看到ALTER TABLE语句、ADD COLUMN `coupon` VARCHAR(50)
首先关系先被禁用,然后再注册表中添加一个新列,最后又启用了这些关系
⑧可以执行脚本查看变动
更好的方法是将脚本保存在我们的文件系统中,然后录入GIT这样的源代码控制资源库中,这样可以明确我们对数据库做了什么变动,然后可以将相同的变动复制到其他数据库,我们只需要在那些数据库服务器中执行那个脚本
(本案例中暂不用管这些,直接执行)
18、REVERSE ENGINEERING A DATABASE——模型的逆向工程
❗如果想更改一个没有模型的数据库怎么办
比如sql_store,这个数据不想school那么全面,如何更改这个数据库
第一次需要这么做的时候,为这个数据库进行逆向工程,来创建一个模型,然后使用该模型用于任何未来更改
①先关闭创建的模型
非常重要,当我们对这个数据库进行逆向工程,如果一个模型已经打开了,MySQL工作台会把数据添加到这个模型上
虽然无大碍,但最好还是为每个数据库配备单独模型,可以在一个模型中包含多个数据库,这些数据库高度相关的时候这么做,并且像在同一个模型中使用它们
②数据菜单中打开逆向工程
③MySQL工作台自动创建了一个新模型
模型中没有任何图,所以在窗口中选择连接
④选择你想要进行逆向工程的数据库sql_store
可以选择多个数据库
重申:如果这些数据库确实高度相关,又想在同一个模型中使用它们再这么做
⑤是否把数据库中全部7张表都导入
同样可以选择显示过滤器来排除一张或多张表
目前想添加所有表到模型中
⑥现在有一个新图,被添加到我们的新模型中
图中可以看到所有表以及它们的关系
这对理解我们的数据库设计非常有帮助
例如产品和订单项目之间有一对多的关系
也可以使用这些图来识别设计中的问题,并允许我们对设计进行任何可能的更改,然后生成更改的代码,以便在别的MySQL数据库中执行
🔺例如订单项目注释表,这张表和任何其他表都不相关
问题:可以在order_id列插入任何值,而MySQL工作台却不会验证它,因此我们确实了数据完整性
当我们在表之间添加关系时,MySQL工作台会强制保证数据的完整性
所以子表中,只能添加与母表中的值相对应的值,目前这里没有任何关系,这意味着可以在order_id列和product_id列中插入无效值
————TBC