浅谈数据库设计范式

    最近也是因为在做之前老项目的重构,由于没有数据库模型、没有相关文档,以至于新上手时临近崩溃状态(要有模型要有文档!要有模型要有文档!要有模型要有文档!)。后几经折腾,终于画出了数据库模型,原本以为是“守得云开见月明”,却是“图样图森破”,总之就是一句话:没有不存在的数据关系,只有想不到的数据结构。也正是这个原因,又重新复习了下数据库范式,一来算是给自己在重构过程中提供更好的优化思路,二来也算是温故而知新了。

                                                                 ——写在前

什么是数据库范式

    从我个人的理解来说,所谓的数据库范式其实就是我们在做数据库模型设计时应该遵循的一些原则和规范。请注意“遵循”这个词,而不是“遵守”。范式的存在并不是说我们一定非要在做数据库模型时按照所谓的“第一范式”、“第二范式”、“第三范式”的去做,它只是给我们提供了一个参考的依据,就像程序中的“驼峰命名规范”,并不是说强制要求就需要这么做,只是说,这么做大家比较容易接受。当然,也有不按照所谓的规范去做的,who care呢,“适合自己才是最重要的”。
    那么按照要求颗粒度的粗细,目前有迹可循的,据说达到了8种(哦,天呐)。我们今天要讨论的,最多也就是前4种,这也是目前大家比较能认可的、适合我们的规范。

第一范式(1NF)

    所谓数据库第一范式(1NF),是……(以下省略若干网站若干书籍上的几百字),NONONO,这不是我们应该看到的文字,程序猿的世界不应该玩概念,通俗易懂就好。一句话,所谓第一范式,就是:禁止“一词多义”。也就是说,在数据库设计时,某一个属性(也就是column字段名啦)所代表的含义能够准确、清晰。假如某个column名叫“name”的属性存储的值如下:“China”,“Java”,“张三”,“代数”……天呐,我会疯的,因为我根本就不知道你的这个name到底是想存什么name。
    还有一种情况,在我们日常的工作中,可能会遇到要存储类似证件类型、证件号码、联系电话等等这样的信息。证件类型是不是与证件号码一对一,但是一个名下是不是会有多种证件信息,联系电话又有固话、移动电话、办公室电话,以及商品信息中会包含着这个商品的一些描述信息,原料组成、适用者对象等等,你们会怎么保存?一个“原料”字段保存所有?创建“固话”,“移动电话”,“身份证”,“护照”等字段信息?OK,完全满足1NF,但是有什么弊端呢?某一天,你们公司来了一位特殊的客户,他持有“联合国认证身份ID”证件,该客户还是你们公司的VVVVIP客户,那么,这个证件信息存哪?领导,今晚紧急上线,因为我们有个VVVVIP客户,我得修改客户信息表,追加一个“联合国认证身份ID”字段……好了,后面不说了,客户信息表是你们的基本业务数据表,测试表示,需要对涉及到的点进行通测,你们开始排期吧。形如还有用一个“原料”、“适用者对象”字段保存了多个值的,后期谁改谁痛苦,你的字段长度不知道什么时候就需要增加。
    所以,请永远!永远!永远记着,1NF要求不能“一词多义”,也不能“一义多词”,所以,在第一范式里,如果出现了这样的情况(特别是形如使用逗号分隔的多值),请!拆!分!请!合!并!

第二范式(2NF)

    第二范式是在第一范式的基础上进行了更细的要求划分。也就是说,所谓的“第二范式”的规范,除了要满足1NF的要求之外,还需要满足以下两点:

  • 记录值可通过某个“属性”或某个“属性组”唯一标识
  • “完全”依赖主关键字,不能“部分”依赖;

    下面解释下上面这2行聱牙佶屈([áo yá jí qū])的意思。
    第一句,说白了,意思就是,数据库里的一条记录,必须得通过某一个字段或者是某几个字段组合起来被查询到。也就是说,你可以“WHERE id = ?”顺利的定位到这条记录,你也可以“WHERE name =? AND age = ? AND sex = ? AND class = ?…”无所谓啊,总之,就要找到这条记录,不能说经过若干个条件组合之后发现,你查询出来的数据是多条。这样,是不满足第二范式要求的。
    第二句,简单粗暴的说,就是你的数据库模型得有“主键”这个玩意儿(主键这个东西的出现,其实也顺利的满足了第一条要求)。有些人可能会说,对不起,我们数据库里是联合主键,即“商品ID+所属库ID”这种形式来确定一条商品信息数据的。那么这个时候,假设有“商品进项价格”这么一个字段值,你就会发现,商品价格这个字段,其实是商品信息的一个属性,它是依赖于商品ID而不依赖于所属库ID的,这种情况下,我们叫商品进项价格“部分”依赖主键。因为商品的进项价格只有一个,与所属库ID无关。那么如果是这种情况,也请拆分。为什么?因为你哪天要更新一个商品进项价格时,你需要更新多行记录!!!
    所以,在第二范式里,你可以认为,第二范式的出现,其实是要求了我们在创建数据库模型时,必须存在有所谓的“主键”属性。

第三范式(3NF)

    第三范式是目前应用比较常见的一种规范,它除了要求必须符合2NF之外,更多的其实是对2NF的细化,是2NF的子集(其实,说到这里,细心的人会发现,当前范式都是必须要满足上一范式的,也就是说,当前范式都是对上一范式的细化,是上一范式的子集)。
    在第三范式中,一句话的解释:不是当前属性实体的属性请不要冗余!按照教科书般的说法就是:任何非主属性不依赖与其他非主属性(我也不知道这到底是个啥)。也就是说,我的是我的,你的是你的,请不要把你的东西放到我这里来。例如:在刚才2NF里我们说到,有一个商品ID和所属库ID关联的关系表(符合2NF规范之后的修改),在这个表里,它记录的是什么?是商品ID和所属库ID的关系,是商品ID,不是商品信息,你不能把什么商品名称啊,价格啊这些属性放到这个表里来(其实他们应该是存在于商品信息表的)。
    说到这里,可能有人会说,不对啊,这种规范刚才在2NF讲过了,怎么3NF还讲呢?注意,请注意区分2NF和3NF的区别:2NF是看是否依赖了主关键字,3NF不仅要看是否依赖了主关键字,还要看是否依赖了非主关键字。在上段提及到的商品与所属库关联表的例子,主关键字是什么?是这个关联表的主键ID,而商品ID、所属库ID是外键,是非主关键字。
下面画个表,大家自己体会下2NF和3NF的区别:

商品ID所属库ID库存数商品名称
c1w110商品1
c2w15商品1
c1w230商品2
c2w220商品2

    这种设计是违反2NF,因为商品名称“部分依赖”了商品ID。所以,如果删除了“商品名称”这个字段,那么这个表是符合2NF规范的。而库存数是只有在商品ID和所属库ID确定时,这个数值才能确定,因为它对于主关键字(即商品ID和所属库ID的复合主键)是完全依赖的,少了任何一个,这个数据没办法确定。当然还有一种改法,就是加入主键ID(PK),这样可以不用删除商品名称,也是可以满足2NF的,如下表所示

主键ID商品ID所属库ID库存数商品名称
1c1w110商品1
2c2w15商品1
3c1w230商品2
4c2w220商品2

    但这种设计却违反了3NF,因为:1、商品名称有冗余,它不属于关联关系表的属性;2、商品名称是依赖于商品ID的,而商品ID是依赖于主键ID的,这种现象叫做“传递依赖”。

Boyce-Codd范式(BCNF)

    说真的,之前好像没有听说过这个东西(大学的数据库设计好像没怎么认真上过),而且在我第一次查阅相关资料时,也是一头雾水。先来看教科书般的说法:如果对于关系模式R中存在的任意一个非平凡函数依赖X->A,都满足X是R的一个超键,那么关系模式R就属于BCNF(正所谓概念害死人,这句话看完,我完全进入了“我是谁,我在哪,我要干什么”的状态)。好了,忘记上面所的,我们回归话题。从一定程度上讲,3NF和BCNF往往特别容易混淆,这也是我刚开始在学习这块时一直陷入其中的,但是他俩还是有着一个最明显的区别,大家只要抓住这个区别,BCNF和3NF还是很好区别的,先来看一张表

主键ID商品ID所属库ID默认发货仓ID库存数
1c1w1df110
2c2w1df15
3c1w2df230
4c2w2df220

    在上表所示中,每个库都会有一个默认的发货仓,这种对应关系为一对一,我们先来看它是否满足3NF。先看主关键字可以有哪些(不清楚主关键字的请回头仔细阅读2NF):主键ID,商品ID和所属库ID,商品ID和默认发货仓ID:非主关键字:数量。那是否满足3NF呢?数量这个字段只有在主关键字全部确认时,才能正确被标识,所以这个表示符合3NF的。那满足2NF么?默认发货仓ID只有在主键ID或者是商品ID、所属库ID确认的情况下才能确认,所以满足2NF(NONONO,有人会说,我只要确认了所属库ID,就可以确认默认发货仓ID了,图样图森破,所属库ID不是主关键字)。但是,各位请思考一个问题,如果出现了某个商品在某个库里没有时,那么请问,这个库的默认发货仓ID是哪个?我们不难发现,默认发货仓ID其实是在依赖着库ID存在的,如果某个库ID没有跟任何商品ID关联,那么我们是无法辨识出库ID和默认发货仓ID的关系的,而像这样的属性存在着对主关键字依赖的,就是不符合BCNF的。
    说到这里,可能有些人就会明白,3NF和BCNF最主要的一个区别就是:我们要去看表里是否存在着多个对应关系(上表所示就是在一个多对多的关联关系表中存在了一对一的关系);如果存在有多个对应关系,那一定要清晰的去分析,哪些是主关键字,哪些是主属性(把主属性按照一定规则组合起来就是主关键字),属性间是否有相互依赖,特别是主属性与主属性之间的(常见于一对一关系隐藏在一对多或者多对多关系中)。
    BCNF与3NF在一定程度上不太好界定,下面给出两者区别,加深下我们的理解:3NF是说“任何非主属性不依赖与其他非主属性”,但是它却允许主关键字被非主属性决定,就像3NF结尾举例的表2,我们不知道主键ID,但是可以通过商品ID和所属库ID推导出主键ID,这叫主属性被非主属性决定。而BCNF强调的确实,任何属性都不能被非主属性决定,是任何!

第四范式(4NF)

    其实说到4NF,我个人认为这才是所谓的数据库模型标准。它最主要的核心思想是,打破多对多关系(NONONO,不是你想的那样,我第一次接触时跟你想的是一样的,相信我,概念玩死人)。来看教科书般的说法:当且仅当对于任意一个非平凡的多值依赖X ↠ Y, X是一个超键。持续懵逼ing。。。其实翻译过来也就一句话:禁止不相关联的属性形成强依赖关系。什么?还不懂,来看一张表

主键ID商品ID所属库ID所属库位ID
1c1w1loc1
2c1w2loc1
3c1w2loc2
4c2w1loc1
5c2w2loc1
6c2w1loc2

    在上图所示的数据表中,其实库和库位的关系是一个多对多的关系(可能有些读者看到这里有些不清楚,我们这么解释下,这里的库是指虚拟库,比如说:某宝、某猫、某东,而这个库位是指仓库的真实地理位置,在不考虑库位库存的情况下,那么在某宝上买c1商品的用户会来自五湖四海,那么我们就要根据实际的收货地址从其所在的或者相邻的省份发货)。那么我们假设出现了这么一个情况:某库位由于管理人员的严重失职,被一把无情的大火烧了,那么请问,你的库存表将面临什么样的灾难(嗯,是的,灾难,我个人认为任何由于数据库模型设计不合理导致后期业务变化而不得不修改数据时,都可以称为灾难)?你需要update掉所有库存表中所有“所属库位ID=失火库位”的全部数据(大概数量是:配置了这个库位的所属库个数*在这个库位里的商品数量)。或者有人说,我们会在每条库存明细的后面增加一个“调整库存”的功能。大哥,你放过程序猿,他宁可写一条update语句。为什么我们会出现这样的情况?这不是我们想要的!我想要的仅仅是把这个失火库位从所属库里关闭掉即可。OK,那么请断开所谓的商品和所属库位的关系,将所属库和库位的关联关系新建一张表,上表需变更为

主键ID商品ID所属库ID
1c1w1
2c1w2
3c2w1
4c2w2
主键ID所属库ID所属库位ID
1w1loc1
2w2loc1
3w2loc2

(请仔细核对两表的差别)

第五、六、七、八范式

    算了吧,省省吧,干嘛要跟自己较劲儿,适合自己的就好,前面4个够用了。

后续

    说到这里,再说点没用的废话。在我们实际工作中,其实并没有一个标准,说某一个数据库模型一定要遵守某个范式,更多的时候,在一个数据库模型里会混用多种设计范式,甚至于有些时候还会发现有些数据库是细于BCNF但是却粗于4NF,甚至于还会出现介于3NF和4NF之间的但是却不同于BCNF的。归根结底,是要看业务复杂度及其数据量的大小,就像设计模式一样,只是给大家提供了一种解决问题的思路,还是那句话“适合自己,才是最好的”。
    可能会有人有这样的疑问:我们为什么要使用范式。举例来说,如果某张表的设计违背了3NF,那么会出现什么情况呢?当主体表更新数据后(即商品信息表更新商品名称),我们需要联动着更新关联表(商品库存关联表)中冗余的数据。如果你没有及时更新,恰巧某个功能接口向外吐的数据取自这个关联表,那么将会是件怎么样的事情呢?是否需要重新翻查代码查询该字段的来源呢?
    另外,请大家思考一个问题:在我们实际项目中,可能会出现类似快照信息的数据表,而这种数据表设计的出现,是应该遵守设计范式还是说,我们可以直接采用KV结构的存储即可?
    最后,感谢你在百忙之中看我这废话连篇的文章,有问题欢迎随时斧正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值