0 前言

学习GIS开发逃不脱的一个部分是空间数据库,但是很多空间数据库的书籍和老师更多着重于“空间数据”这块,对于空间数据库的前置知识讲授较少,因此笔者在此试图以相对形象的情景化方式对相关基本概念做一个小小的总结,一步步介绍如何设计出一个“好”的数据库。

由于本文是以一个情景式的任务逐步介绍数据库的创建和优化,因此建议读者按顺序完整阅读,以便完整理解整个逻辑,若读者朋友希望直接查询自己感兴趣的概念含义,建议直接查阅相关文档或是搜索引擎而非在本文中查找。

1 建一个基础数据库

1.1 统一一下语言

现在你是一个图书馆的管理员,现在需要用电脑管理图书馆的数据,于是很自然地就想到了建一个excel表格,恭喜你,你已经建立一个【数据表】,这个数据表里面记录了很多行的数据,每一行你可以称之为【元组】,例如(PostgreSQL,人民出版社,张三)是一个元组;具体的某个值你可以称为是【分量】,例如称“人民出版社”是一个分量。

 

但你发现,除了图书信息需要记录以外,还有图书馆工作人员的信息也要登记,于是你又新建了一个工作表,记录工作人员的信息,就像这样

 

于是你就初步地建立了一个【数据库】了,一个数据库里面有多个数据表。

1.2 防止乱填数据

创建好之后,你把这个excel表格设为共享文档,发送给同事们编辑,为了防止同事们乱填,你打算限制一下他们能填的内容,比如说“性别”里面只能填男或女,年龄里面只能填一个正整数,让excel来检查,若是不符合要求的话就无法填写,其实这时候你就发明了【数据库完整性校验】,这个“男”或“女”也被称为【域】,其他同事填的内容不能是域以外的,像是如果在性别里面填了个第三性别,那么excel就会报错并且不录入该数据。

 

为了防止数据泄露,你给共享文档设置了一个密码,输对密码才能打开查看或编辑,这就是【数据库的安全性】

 

1.3 正式开始工作

Excel毕竟只是个电子表格软件,既然数据库的一些基础概念已经理解了,我们是时候在真正的数据库里面建真正的表了,打开pgadmin,创建一个表

给它起个名字,就叫“工作人员”吧,新建三列,分别是姓名、年龄、性别,数据类型正如我们前面所说年龄只能是个整数,所以我们选择integer;计算机不能理解“男”和“女”,但是能理解布尔值True和False,刚好能对应上我们的需求(即只能二选一),所以性别的类型选为布尔值boolean或许是个不错的主意。

注:关于整形、布尔型(boolean)可能涉及到一些计算机编程的基础知识,如果你不理解,可以直接按图示设置继续往下阅读,这不会影响后面的介绍。

 

表格建立好之后,我们便可以把数据录入到表格中,我们重点在于厘清概念,所以录入数据的操作这里就不赘述了,你可以通过SQL语句来方便地实现。

 

这时数据库终于是运行起来了,你想要读取出一个同事的数据,相信你很容易就想到根据姓名来获取了,例如需要找张六的数据,这个过程可以理解为:打开数据表,逐行去看此行数据的姓名是否为张六,如果是就取出来,这个过程可以称为是【索引】。听上去不会有什么问题。结果某天有位新同事来入职,他的名字也叫张六,你轻车熟路地也把他的信息录入到了数据库里,就像这样:

后来领导问你新来的张六年龄是多少,你仍然按照原先的思路打开数据表,逐行去看此行数据的姓名是否为张六,如果是就取出来,结果发现取出了两行姓名都为张六的数据,那么年龄到底是26还是18呢?你无从回答。那天的经历让你差点丢掉了饭碗,总结发现这是因为姓名可能出现相同的情况,你痛定思痛,决定一定要搞一个每人不一样的信息,一拍脑袋,想到“工号”或许不错,以后找数据就根据工号来索引数据,这就是【主键】,当你设置工号为主键后,数据库系统就会自动帮你检查每一个录入的员工信息里面,工号都互不重复而且不能是空的,想想如果工号重复了或者某个同事的工号是空的,那就没法根据工号来找到这个同事的数据了。

 

现在数据库已经能很顺利地运行了。你还记得开头我们还创建了一个图书信息的表吗?看到员工数据库上线运行的效果这么好,图书馆希望把图书也入库管理,这时你已经经验丰富,三两下就把图书表格录入到了数据库里,并且还吸取前面的经验教训,细心地增加了“图书编号”作为主键,这样就不怕找不到同名的书等情况发生了。

 

2 烦人的相互关系

2.1 一对多关系

后来图书馆买了一本新书,图书的信息如下表所示

图书名称

出版社

作者

MySQL原理详解

电子出版社

张小明、李小华

你照常把它录入到了数据库中,图书馆逐步壮大。现在图书馆打算邀请一些知名的作者前来作公益讲座,为了方便联系,需要把作者的姓名、电话号码给存储起来。前几本图书都没有问题,对于新录入的这本图书,如果像是下表这样存数据显然是不合理的,我们没法知道哪个电话号码是对应哪位作者的;此外,像是张三这样的,既是1的作者也是4的作者,他的联系电话被重复地存储了,浪费了宝贵的存储空间。

图书编号

图书名称

出版社

作者

联系电话

1

PostgreSQL入门

人民出版社

张三

111

2

Java从入门到精通

电子出版社

李四

222

3

Postgis空间数据库

科技出版社

王五

333

4

MySQL原理详解

电子出版社

张小明、李小华

132,136

现实中,作者和图书之间本身就是相对独立的,像是这张表里面一本书就对应一个或两个坐着,将图书与作者强行绑定在一块儿了,不难想到我们可以把作者相关的信息拆出来,再建一个表来存储不就行了吗?就像这样:(注意别忘了前面学习到的“主键”,给每个作者编个号)

 

 

问题是,现在怎么知道图书对应的作者是谁呢?想想既然作者表中已经为每位作者编了个号,那我们在图书表中加一列,记录这本书对应的作者是不是就可以了?就像下图这样。

 

如此一来,咱们只需要逐行把图书表中的数据取出来,读出出对应的作者编号,然后拿着这个作者编号去到作者表里面同样方法再找,就能找到这个作者的姓名和联系电话了。

2.2 多对多关系

但是,检索到第4本书的时候发现有点不对劲儿:我们检索出来对应的作者编号是“4,5”这整个字符串,作者表里面显然没有“4,5”这个编号的作者,只有4号作者和5号作者。你或许会想到用逗号分隔把“4,5”这个字符串转为一个数组[4,5],再遍历该数组,但是如此复杂的操作势必会降低数据库查询的效率,而且每一行的作者编号这一栏存储的长度都不一样,数据结构不是很好。聪明的你应该能想到:既然记录“图书4由4、5两个作者创作”不行,那反过来记录“4、5两个作者创作了图书4”不就行了吗?这其实就是【一对多】关系在数据库实际存储的情况,就像下表这样。

 

 

作者表中的“出版图书”称为作者表的【外键】,它里面填的值对应着的就是图书表的【主键】,这样就能根据出版图书里面的值再去图书表里面准确地找到这个作者对应的那本书了。反过来,也可以根据图书表里面的主键(图书编号)回头去作者表里面查“所有出版图书等于4”的记录,也就是这本书对应的所有作者信息了。

正以为大功告成之际,图书馆又买回来了一本书,图书信息是这样的

图书名称

出版社

作者

PostgreSQL进阶

邮电出版社

张三

按照刚刚所说的逻辑,如果要录入这本书,那就得像这样子记录

 

好家伙,刚刚好不容易解决的一对多问题又出现了,张三对应的“出版图书”有两本。采购员采购一时爽,整得我们是又得加班了。冷静一下,我们来理一理现在的情况:正着去想,4号图书有两位作者;反着想,张三这位作者有两本书。可谓是腹背受敌,进退两难呐,此时原来的“一对多”关系已经演变成为了“多对多”的关系。再仔细想想,刚刚用两张表就可以解决一个一对多关系的问题,那现在用三张表不就可以解决多对多关系问题了吗?真是个天才的想法。这样一来,查询的过程就会像下图这样:

 

要找张三出版的书,那就先在下方的表找相应的图书编号,得到图书编号后再去图书表里面相应找图书的详细信息。目前来看,下方的这个辅助我们解决问题的表是我们虚构出来的,它有没有什么实际意义呢?毕竟表里头就只有两个有用的字段,不难看出,是“作者编写图书”,所以这个表我们不妨就认为是编撰记录表,我们也可以进一步去丰富它,补充一些关于“编撰”这个过程的信息,实现更多的功能。总而言之,这“一对多”的关系终于是被搞定了。我们再来复习一下:这个“编撰”表中存储的作者、图书两个字段都是外键,而作者表里的作者编号和图书表里的图书编号则是主键,也就是说编撰表里面的作者字段,其值是引用了作者表中的作者编号的对应内容,主键、外键或许比较难以理解,你可能得通过更多的例子和实践进一步加深对它的理解。

 

3 能不能未卜先知

像上面这样,一套简简单单的图书管理系统,经过好几个星期的运行,不断遇到问题再调整数据库的设计以解决问题。我们便会思考:后来所做的这些调整能不能在一开始设计的时候就做好呢?毕竟不是哪位领导都跟咱们的馆长一样愿意拿一套正式投入使用的系统来做小白鼠的。于是,前人经过大量的实践,为我们总结出了一份“检查清单”,让我们在设计数据库时对照着检查,这样就能最大程度地避免到投入使用时才发现问题了,这份“检查清单”就是数据库【范式】,只要我们按着范式来设计就能避免很多不必要的返工。

3.1 存的数据不能再分

回忆一下我们刚刚遇到的这个问题:

 

4号图书有两位作者,后来我们发现像这样在一个值里面填两个值,在后面的操作中会遇到问题(不记得了的话可以往前翻一下,这里不再重复叙述),所以关系数据库的【第一范式】就是说每个属性不能包含多个值,也就是上面这个表格里面每个单元格里面不能有多值。细心的你在练习过程中可能发现了:像是postgres这样的数据库提供了数组、json等数据类型,也就是一个字段里面可以存很复杂的数据结构,但是一般而言不建议采用数组、json等方式来存储的。如果有某个字段内容是多个值,正确的做法应该是另外建一个表,通过一对多的关系来表达。

3.2 主键的作用得发挥出来

我们还是得回顾一下最开始时遇到的一个问题:

 

在这个表中,老板问新来的张六年龄是多少,在这个表中即使确定了“姓名=张六”这样一个条件,仍然无法唯一确定是哪条数据,想确定年龄和性别更加是无从谈起。因此我们需要把主键设置为一个能与数据一一对应的字段,由于我们这个表里面原来的三个字段都存在重复的可能性,所以我们当时就新建了一个“员工编号”的字段,来做为新的主键,如此一来,只要员工编号确定了,其它属性(称为【非主属性】)就都完全确定了,这就是关系数据库【第二范式】的要求,也就是说所有非主属性【完全依赖】于主键,不要让主键成为了空头司令。

3.3 一步直达别绕弯

很幸运,之前我们构建数据库的整个过程中没有遇到这样的问题,或者说是因为聪明的你凭着本能已经避开了这样的问题。

 

像是这样一个表里面,显然主键是图书编号,假如说我们在设计的时候认为作者由出版社决定,出版社由图书决定,这样的逻辑称为【传递函数依赖】,也就是作者绕了个弯依赖于主键,此时就违反了关系数据库的【第三范式】,换而言之,我们设计时就需要确保每一行的每一个数据都直接由主键决定。此外,第三范式中还提到了一个【部分函数依赖】,这个涉及到由多个字段共同作为主键的情况,而实际设计过程中我们一般应当设计一个唯一的主键,不应该把多个字段组合作为主键,这样也就不会出现部分函数依赖的情况了。

 3.4 只有主键是唯一不重复的

同样的,我们所设计的数据库业务比较简单,没有机会让我们能够制造这样的问题。假如说我们的员工表里面存的数据是这样的

 

根据我们的经验可以知道身份证号是唯一的,每个人之间是不会重复的,这时我们就没必要再新建一个叫做“员工编号”的字段来做主键了,直接拿身份证号来作为主键就行,这样子就能少存一列数据,节约一些存储空间,同时不影响数据库原来的正常运行。这就是【BC范式】所说的事情,它要求每个表里面只有主键那个字段是每条里面各不相同的。但是,我们实际业务中难免会遇到不止一条互不相同的情况,比如说上面这个表既要存员工的身份证号又要存员工的工资银行卡号,按照BC范式的规定,我们就应当把这个表拆成两个表来确保每个表里只有一个字段(也就是主键那个字段)是互不重复的。显然,这么做反而降低了数据查询的效率,想要查某个员工的工资卡号还要进行多表查询,整个数据库里面的各种一对多关系也变得十分复杂,所以BC范式很多时候需要我们根据实际情况来决定是否完全遵守。

 

4 结语

关于关系型数据库的一些基本概念到这里就基本带过一遍了,本文初衷是以实际情景让读者更好地理解关系型数据库的部分基本概念,从而更好地接下去进行进一步的学习,因此只涉及到了部分比较常见到的概念,建议读者后续再阅读更多专业的书籍文献等,以形成对关系型数据库的完整认识。受限于读者的知识水平,文中难免有错漏,欢迎各位读者批评指正。