时间:2021-07-01 10:21:17 帮助过:34人阅读
从笔者的经历看来,笔者更赞成在项目早期由开发者进行数据库设计(后期调优需要DBA)。根据笔者的项目经验,一个精通OOP和ORM的开发者,设计的数据库往往更为合理,更能适应需求的变化,如果追其原因,笔者个人猜测是因为数据库的规范化,与OO的部分思想雷同(如内聚)。而DBA,设计的数据库的优势是能将DBMS的能力发挥到极致,能够使用SQL和DBMS实现很多程序实现的逻辑,与开发者相比,DBA优化过的数据库更为高效和稳定。如标题所示,本文旨在分享一名开发者的数据库设计经验,并不涉及复杂的SQL语句或DBMS使用,因此也不会局限到某种DBMS产品上。真切地希望这篇文章对开发者能有所帮助,也希望读者能帮助笔者查漏补缺。
Edgar Frank Codd(埃德加·弗兰克·科德)被誉为“关系数据库之父”,并因为在数据库管理系统的理论和实践方面的杰出贡献于1981年获图灵奖。在1985年,Codd 博士发布了12条规则,这些规则简明的定义出一个关系型数据库的理念,它们被作为所有关系数据库系统的设计指导性方针。
(一)规划阶段
规划阶段的主要工作是对数据库的必要性和可行性进行分析。确定是否需要使用数据库,使用哪种类型的数据库,使用哪个数据库产品。
(二)概念阶段
概念阶段的主要工作是收集并分析需求。识别需求,主要是识别数据实体和业务规则。对于一个系统来说,数据库的主要包括业务数据和非业务数据,而业务数据的定义,则依赖于在此阶段对用户需求的分析。需要尽量识别业务实体和业务规则,对系统的整体有初步的认识,并理解数据的流动过程。理论上,该阶段将参考或产出多种文档,比如“用例图”,“数据流图”以及其他一些项目文档。如果能够在该阶段产出这些成果,无疑将会对后期进行莫大的帮助。当然,很多文档已超出数据库设计者的考虑范围。而且,如果你并不精通该领域以及用户的业务,那么请放弃自己独立完成用户需求分析的想法。用户并不是技术专家,而当你自身不能扮演“业务顾问”的角色时,请你选择与项目组的相关人员合作,或者将其视为风险呈报给PM。再次强调,大多数情况,用户只是行业从业者,而非职业技术人员,我们仅仅从用户那里收集需求,而非依赖于用户的知识。
记录用户需求时,可以使用一些技巧,当然这部分内容有些可能会超出数据库设计人员的职责:
此外,必须严谨处理业务规则,并详细记录。在之后的阶段,将会根据这些业务规则进行设计。
当该阶段结束时,你应该能够回答以下问题:
并且得到如下信息:
(三)逻辑阶段
逻辑阶段的主要工作是绘制E-R图,或者说是建模。建模工具很多,有不同的图形表示方法和软件。这些工具和软件的使用并非关键,笔者也不建议读者花大量时间在建模方法的选择上。对于大多数应用来说,E-R图足以描述实体间的关系。建模关键是思想而不是工具,软件只是起到辅助作用,识别实体关系才是本阶段的重点。
除了实体关系,我们还应该考虑属性的域(值类型、范围、约束)
(四)实现阶段
实现阶段主要针对选择的RDBMS定义E-R图对应的表,考虑属性类型和范围以及约束。
(五)物理阶段
物理阶段是一个验证并调优的阶段,是在实际物理设备上部署数据库,并进行测试和调优。
(一)降低对数据库功能的依赖
功能应该由程序实现,而非DB实现。原因在于,如果功能由DB实现时,一旦更换的DBMS不如之前的系统强大,不能实现某些功能,这时我们将不得不去修改代码。所以,为了杜绝此类情况的发生,功能应该有程序实现,数据库仅仅负责数据的存储,以达到最低的耦合。
(二)定义实体关系的原则
当定义一个实体与其他实体之间的关系时,需要考量如下:
关系与表数量
(三)列意味着唯一的值
如果表示坐标(0,0),应该使用两列表示,而不是将“0,0”放在1个列中。
(四)列的顺序
列的顺序对于表来说无关紧要,但是从习惯上来说,采用“主键+外键+实体数据+非实体数据”这样的顺序对列进行排序显然能得到比较好的可读性。
(五)定义主键和外键
数据表必须定义主键和外键(如果有外键)。定义主键和外键不仅是RDBMS的要求,同时也是开发的要求。几乎所有的代码生成器都需要这些信息来生成常用方法的代码(包括SQL文和引用),所以,定义主键和外键在开发阶段是必须的。之所以说在开发阶段是必须的是因为,有不少团队出于性能考虑会在进行大量测试后,在保证参照完整性不会出现大的缺陷后,会删除掉DB的所有外键,以达到最优性能。笔者认为,在性能没有出现问题时应该保留外键,而即便性能真的出现问题,也应该对SQL文进行优化,而非放弃外键约束。
(六)选择键
1 人工键与自然键
人工健——实体的非自然属性,根据需要由人强加的,如GUID,其对实体毫无意义;自然健——实体的自然属性,如身份证编号。
人工键的好处:
人工键的缺点:
笔者建议全部使用人工键。原因如下:
笔者的另一个建议是——每张表都需要有一个对用户而言有意义的自然键,在特殊情况下也许找不到这样一个项,此时可以使用复合键。这个键我在程序中并不会使用其作为唯一标识,但是却可以在对数据库直接进行查询时使用。
使用人工键的另一根弊端,主要源自对查询性能的考量,因此选择人工键的形式(列的类型)很重要:
2 智能健与非智能键
智能键——键值包含额外信息,其根据某种约定好的编码规范进行编码,从键值本身可以获取某些信息;非智能键,单纯的无意义键值,如自增的数字或GUID。
智能键是一把双刃剑,开发人员偏爱这种包含信息的键值,程序盼望着其中潜在的数据;数据库管理员或者设计者则讨厌这种智能键,原因也是很显然的,智能键对数据库是潜在的风险。前面提到,数据库设计的原则之一是不要把具有独立意义的值的组合实现到一个单一的列中,应该使用多个独立的列。数据库设计者,更希望开发人员通过拼接多个列来得到智能键,即以复合主键的形式给开发人员使用,而不是将一个列的值分解后使用。开发人员应该接受这种数据库设计,但是很多开发者却想不明白两者的优略。笔者认为,使用单一列实现智能键存在这样一个风险,就是我们可能在设计阶段无法预期到编码规则可能会在后期发生变化。比如,构成智能键的局部键的值用完而引起规则变化或者长度变化,这种编码规则的变化对于程序的有效性验证与智能键解析是破坏性的,这是系统运维人员最不希望看到的。所以笔者建议如果需要智能键,请在业务逻辑层封装(使用只读属性),不要再持久化层实现,以避免上述问题。
(七)是否允许NULL
关于NULL我们需要了解它的几个特性:
那么我们是否应该允许列为空呢?笔者认为这个问题的答案受到我们的开发语言的影响。以C#为例,因为引入了可空类型来处理数据库值类型为NULL的情形,所以是否允许为空对开发者来说意义并不大。但有一点必须注意,就是验证非空必须要在程序集进行处理,而不该依赖于DBMS的非空约束,必须确保完整数据(所有必须的属性均被赋值)到达DB(所谓的“安全区”,我们必须定义在多层系统中那些区域得到的数据是安全而纯净的)。
(八)属性切割
一种错误想法是,属性与列是1:1的关系。对于开发者,我们公开属性而非字段。举个例子来说,对于实体“员工”有“名字”这一属性,“名字”可以再被分解为“姓”和“名”,对于开发人员来说,显然第二种数据结构更受青睐(“姓”和“名”作为两个字段)。所以,在设计时我们也应该根据需要考虑是否切割属性。
(九)规范化——范式
当笔者还在大学时,范式是学习关系型数据库时最头疼的问题。我想也许会有读者仍然不理解范式的价值,简单来说——范式将帮助我们来保证数据的有效性和完整性。规范化的目的如下:
规范化旨在——挑出复杂的实体,从中抽取出简单的实体。这个过程一直持续下去,直到数据库中每个表都只代表一件事物,并且表中每个描述的都是这件事物为止。
1 规范化实体和属性(去除冗余)
1NF:每个属性都只应表示一个单一的值,而非多个值。
需要考虑几点:
当前设计不符合1NF的“臭味”:
2 属性间的关系(去除冗余)
2NF-实体必须符合1NF,每个属性描述的东西都必须针对整个键(可以理解为oop中类型属性的内聚性)。
当前设计不符合2NF的“臭味”:
3NF-实体必须符合2NF,非键属性不能描述其他非键属性。(与2NF不同,3NF处理的是非键属性和非键属性之间的关系,而不是和键属性之间的关系。
当前设计不符合3NF的“臭味”:
BCNF-实体满足第一范式,所有属性完全依赖于某个键,如果所有的判定都是一个键,则实体满足BCNF。(BCNF简单地扩展了以前的范式,它说的是:一个实体可能有若干个键,所有属性都必须依赖于这些键中的一个,也可以理解为“每个键必须唯一标识实体,每个非键熟悉必须描述实体。”
3 去除实体组合键中的冗余
4NF-实体必须满足BCNF,在一个属性与实体的键之间,多值依赖(一条记录在整个表的唯一性由多个值组合起来决定的)不能超过一个。
当前设计不符合4NF的“臭味”:
4 尽量将所有关系分解为二元关系
5NF-实体必须满足4NF,当分解的信息无损的时候,确保所有关系都被分解为二元关系。
5NF保证在第四范式中存在的任何可以分解为实体的三元关系都被分解。有的三元关系可以在不丢失信息的前提下被分解为二元关系,当分解为两个二元关系的过程要丢失信息时,关系被宣称为处于第四范式中。所以,第五范式建议是,最好把现有的三元关系都分解为3个二元关系。
需要注意的是,规范化的结果可能是更多的表,更复杂的查询。因此,处理到何种程度,取决于性能和数据架构的多方考量。建议规范化到第四范式,原因是5NF的判断太过隐晦。例如:表X(老师,学生,课程)是一个三元关系,可以分解为表A(老师,学生),表B(学生,课程),表C(老师,课程)。表X表示某个老师是上某个学生的某个课程的老师;表A表示老师教学生;表B表示学生上课;表C表示老师教课。单独看是无法发现问题的,但是从数据出发,"表X=表A+表B+表C"并不一定成立,即不能通过连接构建分解前的数据。因为可能有多种组合,丧失了表X反馈出的业务规则。这种现象,容易在设计阶段被忽略,但好在在开放阶段会被显现,而且并不经常发生。
推荐做法:
(十)选择数据类型(MS SQL 2008)
MS SQL的常用类型:
精确数字 | 不会发生精度损失 | bit tinyint smallint int bigint decimal |
近似数字 | 对于极值可能发生精度损失 | float(N) real |
日期和时间 | date time smalldatetime datetime datetime2 datetimeoffset | |
二进制数据 | bingary(N) varbinary(N) varbinary(max) | |
字符(串)数据 | char(N) varchar(N) varchar(max) nchar(N) nvarchar(N) nvarchar(max) | |
存储任意数据 | sql_variant | |
时间戳 | timestamp | |
GUID | uniqueidentifier | |
XML | 不要试图使用该类型规避1NF | xml |
空间数据 | geometry geography | |
层次数据 | heirarchyid |
MS SQL中不在支持的或糟糕的类型选择
常用类型选择:
类型选择的最基本规则是选择满足需要的最轻的类型,因为这样查询更快。
bool | 建议使用bit而非char(1),因为开发语言对其支持觉好,可以直接映射为bool或bool?。 |
大值数据 | 使用所有备选类型中最小的那种,类型越大,查询越慢,当字节大于8000时,应使用max。 |
主键 | 自增主键根据预期范围选择int或bigint,GUID使用uniqueidentifier而非varchar(N)。 |
(十一)优化并行
设计DB时就应该考虑到对并行进行优化,比如,MS SQL中的timestamp类型就是极好的选择。