从新思考关系型数据库的设计

据库设计,一个软件项目成功的基石。不少从业人员都认为,数据库设计其实不那么重要。现实中的情景也至关雷同,开发人员的数量是数据库设计人员的数倍。多数人使用数据库中的一部分,因此也会把数据库设计想的如此简单。其实否则,数据库设计也是门学问。数据库

从笔者的经历看来,笔者更同意在项目早期由开发者进行数据库设计(后期调优须要DBA)。根据笔者的项目经验,一个精通OOP和ORM的开发者,设计的数据库每每更为合理,更能适应需求的变化,若是追其缘由,笔者我的猜想是由于数据库的规范化,与OO的部分思想雷同(如内聚)。而DBA,设计的数据库的优点是能将DBMS的能力发挥到极致,可以使用SQL和DBMS实现不少程序实现的逻辑,与开发者相比,DBA优化过的数据库更为高效和稳定。如标题所示,本文旨在分享一名开发者的数据库设计经验,并不涉及复杂的SQL语句或 DBMS使用,所以也不会局限到某种DBMS产品上。真切地但愿这篇文章对开发者能有所帮助,也但愿读者能帮助笔者查漏补缺。安全

一 Codd的RDBMS12法则——RDBMS的起源数据结构

Edgar Frank Codd(埃德加·弗兰克·科德)被誉为“关系数据库之父”,并由于在数据库管理系统的理论和实践方面的杰出贡献于1981年获图灵奖。在1985 年,Codd 博士发布了12条规则,这些规则简明的定义出一个关系型数据库的理念,它们被做为全部关系数据库系统的设计指导性方针。架构

  1. 信息法则 关系数据库中的全部信息都用惟一的一种方式表示——表中的值。
  2. 保证访问法则 依靠表名、主键值和列名的组合,保证能访问每一个数据项。
  3. 空值的系统化处理 支持空值(NULL),以系统化的方式处理空值,空值不依赖于数据类型。
  4. 基于关系模型的动态联机目录 数据库的描述应该是自描述的,在逻辑级别上和普通数据采用一样的表示方式,即数据库必须含有描述该数据库结构的系统表或者数据库描述信息应该包含在用户能够访问的表中。
  5. 统一的数据子语言法则 一个关系数据库系统能够支持几种语言和多种终端使用方式,但必须至少有一种语言,它的语句可以一某种定义良好的语法表示为字符串,并能全面地支持如下全部规则:数据定义、视图定义、数据操做、约束、受权以及事务。(这种语言就是SQL)
  6. 视图更新法则 全部理论上能够更新的视图也能够由系统更新。
  7. 高级的插入、更新和删除操做 把一个基础关系或派生关系做为单个操做对象处理的能力不只适应于数据的检索,还适用于数据的插入、修改个删除,即在插入、修改和删除操做中数据行被视做集合。
  8. 数据的物理独立性 无论数据库的数据在存储表示或访问方式上怎么变化,应用程序和终端活动都保持着逻辑上的不变性。
  9. 数据的逻辑独立性 当对表作了理论上不会损害信息的改变时,应用程序和终端活动都会保持逻辑上的不变性。
  10. 数据完整性的独立性 专用于某个关系型数据库的完整性约束必须能够用关系数据库子语言定义,并且能够存储在数据目录中,而非程序中。
  11. 分布独立性 无论数据在物理是否分布式存储,或者任什么时候候改变分布策略,RDBMS的数据操纵子语言必须能使应用程序和终端活动保持逻辑上的不变性。
  12. 非破坏性法则 若是一个关系数据库系统支持某种低级(一次处理单个记录)语言,那么这个低级语言不能违反或绕过更高级语言(一次处理多个记录)规定的完整性法则或约束,即用户不能以任何方式违反数据库的约束。

二 关系型数据库设计阶段运维

(一)规划阶段数据库设计

规划阶段的主要工做是对数据库的必要性和可行性进行分析。肯定是否须要使用数据库,使用哪一种类型的数据库,使用哪一个数据库产品。分布式

(二)概念阶段函数

概念阶段的主要工做是收集并分析需求。识别需求,主要是识别数据实体和业务规则。对于一个系统来讲,数据库的主要包括业务数据和非业务数据,而业务数据的定义,则依赖于在此阶段对用户需求的分析。须要尽可能识别业务实体和业务规则,对系统的总体有初步的认识,并理解数据的流动过程。理论上,该阶段将参考或产出多种文档,好比“用例图”,“数据流图”以及其余一些项目文档。若是可以在该阶段产出这些成果,无疑将会对后期进行莫大的帮助。固然,不少文档已超出数据库设计者的考虑范围。并且,若是你并不精通该领域以及用户的业务,那么请放弃本身独立完成用户需求分析的想法。用户并非技术专家,而当你自身不能扮演“业务顾问”的角色时,请你选择与项目组的相关人员合做,或者将其视为风险呈报给PM。再次强调,大多数状况,用户只是行业从业者,而非职业技术人员,咱们仅仅从用户那里收集需求,而非依赖于用户的知识。工具

记录用户需求时,可使用一些技巧,固然这部份内容有些可能会超出数据库设计人员的职责:oop

  • 努力维护一系列包含了系统设计和规格说明信息的文档,如会议记录、访谈记录、关键用户指望、功能规格、技术规格、测试规格等。
  • 频繁与干系人沟通并收集反馈。
  • 标记出你本身添加的,不属于客户要求的,未决内容。
  • 与全部关键干系人尽快确认项目范围,并力求冻结需求。

此外,必须严谨处理业务规则,并详细记录。在以后的阶段,将会根据这些业务规则进行设计。

当该阶段结束时,你应该可以回答如下问题:

  • 须要哪些数据?
  • 数据该被怎样使用?
  • 哪些规则控制着数据的使用?
  • 谁会使用何种数据?
  • 客户想在核心功能界面或者报表上看到哪些内容?
  • 数据如今在哪里?
  • 数据是否与其余系统有交互、集成或同步?
  • 主题数据有哪些?
  • 核心数据价值几何,对可靠性的要求程度?

而且获得以下信息:

  • 实体和关系
  • 属性和域
  • 能够在数据库中强制执行的业务规则
  • 须要使用数据库的业务过程

(三)逻辑阶段

逻辑阶段的主要工做是绘制E-R图,或者说是建模。建模工具不少,有不一样的图形表示方法和软件。这些工具和软件的使用并不是关键,笔者也不建议读者花大量时间在建模方法的选择上。对于大多数应用来讲,E-R图足以描述实体间的关系。建模关键是思想而不是工具,软件只是起到辅助做用,识别实体关系才是本阶段的重点。

除了实体关系,咱们还应该考虑属性的域(值类型、范围、约束)

(四)实现阶段

实现阶段主要针对选择的RDBMS定义E-R图对应的表,考虑属性类型和范围以及约束。

(五)物理阶段

物理阶段是一个验证并调优的阶段,是在实际物理设备上部署数据库,并进行测试和调优。

 

三 设计原则

(一)下降对数据库功能的依赖

功能应该由程序实现,而非DB实现。缘由在于,若是功能由DB实现时,一旦更换的DBMS不如以前的系统强大,不能实现某些功能,这时咱们将不得不去修改代码。因此,为了杜绝此类状况的发生,功能应该有程序实现,数据库仅仅负责数据的存储,以达到最低的耦合。

(二)定义实体关系的原则

当定义一个实体与其余实体之间的关系时,须要考量以下:

  • 牵涉到的实体 识别出关系所涉及的全部实体。
  • 全部权 考虑一个实体“拥有”另外一个实体的状况。
  • 基数 考量一个实体的实例和另外一个实体实例关联的数量。

关系与表数量

  • 描述1:1关系最少须要1张表。
  • 描述1:n关系最少须要2张表。
  • 描述n:n关系最少须要3张表。

(三)列意味着惟一的值

若是表示坐标(0,0),应该使用两列表示,而不是将“0,0”放在1个列中。

(四)列的顺序

列的顺序对于表来讲可有可无,可是从习惯上来讲,采用“主键+外键+实体数据+非实体数据”这样的顺序对列进行排序显然能获得比较好的可读性。

(五)定义主键和外键

数据表必须定义主键和外键(若是有外键)。定义主键和外键不只是RDBMS的要求,同时也是开发的要求。几乎全部的代码生成器都须要这些信息来生成经常使用方法的代码(包括SQL文和引用),因此,定义主键和外键在开发阶段是必须的。之因此说在开发阶段是必须的是由于,有很多团队出于性能考虑会在进行大量测试后,在保证参照完整性不会出现大的缺陷后,会删除掉DB的全部外键,以达到最优性能。笔者认为,在性能没有出现问题时应该保留外键,而即使性能真的出现问题,也应该对SQL文进行优化,而非放弃外键约束。

(六)选择键

1 人工键与天然键

人工健——实体的非天然属性,根据须要由人强加的,如GUID,其对实体毫无心义;天然健——实体的天然属性,如身份证编号。

人工键的好处:

  • 键值永远不变
  • 永远是单列存储

人工键的缺点:

  • 由于人工键是没有实际意义的惟一值,因此不能经过人工键来避免重复行。

笔者建议所有使用人工键。缘由以下:

  • 在设计阶段咱们没法预测到代码真正须要的值,因此干脆放弃猜想键,而使用人工键。
  • 人工键复杂处理实体关系,而不负责任何属性描述,这样的设计使得实体关系与实体内容获得高度解耦,这样作的设计思路更加清晰。

笔者的另外一个建议是——每张表都须要有一个对用户而言有意义的天然键,在特殊状况下也许找不到这样一个项,此时可使用复合键。这个键我在程序中并不会使用其做为惟一标识,可是却能够在对数据库直接进行查询时使用。

使用人工键的另外一根弊端,主要源自对查询性能的考量,所以选择人工键的形式(列的类型)很重要:

  • 自增值类型 因为类型轻巧查询效率更好,但取值有限。
  • GUID 查询效率不如值类型,可是取值无限,且对开发人员更加亲切。

2 智能健与非智能键

智能键——键值包含额外信息,其根据某种约定好的编码规范进行编码,从键值自己能够获取某些信息;非智能键,单纯的无心义键值,如自增的数字或GUID。

智能键是一把双刃剑,开发人员偏心这种包含信息的键值,程序盼望着其中潜在的数据;数据库管理员或者设计者则讨厌这种智能键,缘由也是很显然的,智能键对数据库是潜在的风险。前面提到,数据库设计的原则之一是不要把具备独立意义的值的组合实现到一个单一的列中,应该使用多个独立的列。数据库设计者,更但愿开发人员经过拼接多个列来获得智能键,即以复合主键的形式给开发人员使用,而不是将一个列的值分解后使用。开发人员应该接受这种数据库设计,可是不少开发者却想不明白二者的优略。笔者认为,使用单一列实现智能键存在这样一个风险,就是咱们可能在设计阶段没法预期到编码规则可能会在后期发生变化。好比,构成智能键的局部键的值用完而引发规则变化或者长度变化,这种编码规则的变化对于程序的有效性验证与智能键解析是破坏性的,这是系统运维人员最不但愿看到的。因此笔者建议若是须要智能键,请在业务逻辑层封装(使用只读属性),不要再持久化层实现,以免上述问题。

(七)是否容许NULL

关于NULL咱们须要了解它的几个特性:

  • 任何值和NULL拼接后都为NULL。
  • 全部与NULL进行的数学操做都返回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反馈出的业务规则。这种现象,容易在设计阶段被忽略,但好在在开放阶段会被显现,并且并不常常发生。

推荐作法:

  • 尽量地遵照上述规范化原则。
  • 全部属性描述的都应该是体现被建模实体的本质的内容。
  • 至少必须有一个键,它惟一地标识和描述了所建实体的本质。
  • 主键要谨慎选择。
  • 在逻辑阶段能作多少规范化就作多少(性能不是逻辑阶段考虑的范畴)。
相关文章
相关标签/搜索