对于需要定义模式的数据库来说,具体采取什么样的设计是需要考虑的问题。
本节将专门谈论关系数据库,因为这是执行比较严格的模式(schema)的数据库。其他数据库的模式调整更为灵活,但也能受益于花时间为其设计的结构。
改变模式是一件很重要的事情,需要进行规划,当然,在设计时也应当有长远的打算。
我们将在本章后面讨论如何改变数据库的模式。这里先提醒一下,在构建系统的过程中,数据库模式的改变是不可避免的。然而,在这个过程中,我们需要重视并理解可能出现的问题。花时间思考并确保对模式的良好设计绝对是一个好主意。
开始设计模式的最好方法是绘制不同的表、字段,以及它们之间的关系,如果有指向其他表的外键的话。具体情况如图3-7所示。
图3-7 绘制数据库模式
通过展示这些信息,以便发现其中可能存在的盲点或重复的元素。如果表太多,或许有必要将其分为几组。
虽然有些工具可以帮助完成这项工作,但就我个人而言,手绘这些关系图表有助于思考这些关系,并在脑海中构建系统设计。
每个表都可能与其他表有不同类型的外键关系:
❍ 一对多 (one-to-many),即为另一个表的多个元素添加引用。例如,某个作者在他所有的书中都被引用。在这种情况下,建立一个简单的外键关系是可行的,因为图书表将会有一个外键指向作者表中的条目。多个图书表的记录可以引用同一个作者,如图3-8所示。
❍ 一对零或一 (one-to-zero or one)是特殊情况,即一条记录只能与另一条记录相关。例如,假设出版社的某个编辑在加工某本书(而且同一时刻只能加工一本书)。编辑在书籍表中的引用是一个外键,如果当前没有正在加工的书籍,可以设置为null(空)。另一个从编辑到书的反向引用将确保这种关系是唯一的。这两个引用都需要在同一事务中进行修改,如图3-9所示。
图3-8 第一个表中的键引用第二个表中的多条记录
图3-9 这种关系只能匹配特定的两行记录
严格的一对一关系,比如一本书和一个书名,两者始终都是相关的,通常这种情况下较好的做法是将所有的信息添加到同一个表中。
❍ 多对多 (many-to-many),即双向都可能有多条记录匹配。例如,一本书可能被归类到不同的体裁下,同时一个体裁可能会包含多本书。在关系型数据结构中,需要有一个额外的中间表来建立这种关系,它将同时指向书和体裁两个表,如图3-10所示。
这个额外的中间表可能包括更多的信息,例如,关于书的体裁的详细信息。这样一来,可以用它描述那些50%是恐怖类、90%是冒险类的书。
除了关系数据库之外,有时对创建多对多关系的需求并不那么迫切,而是直接将其作为标签的集合来添加。有些关系数据库现在可以进行更灵活的设置,决定字段是列表值还是JSON对象,通过这种方式来简化设计。
图3-10 注意中间表可能有多种组合。第一个表可以引用第二个表的多条记录,且第二个表可以引用第一个表的多条记录
大多数情况下,每个表存储的字段类型都是很简单易懂的,但还应考虑某些细节问题:
❍ 预留足够的空间以适应未来增长 。有些字段,如字符串,需要定义一个最大的存储尺寸。例如,存储一个代表电子邮件地址的字符串最多需要254个字符。但有时尺寸的需求并不明确,比如存储客户的名字。这种时候,最好是在确保足够的尺寸上再留一定余地。
❍这些限制不仅应该应用于数据库中,而且应该在更高的级别上执行,以便始终让所有访问该字段的API或用户接口能够优雅地处理相关数据。
涉及数值型字段时,在大多数情况下,普通的整数就足以满足各种场景的需要。虽然有些数据库可以使用像smallint这样的两字节或tinyint这样的单字节数值的字段类型,但不建议使用,因为所消耗的空间的差异其实是非常小的。
❍ 数据库在内部采用的数据表示方法无须和外部所用的保持一致 。例如,存储在数据库中的时间通常都是UTC格式的,待使用时再转换成用户时区对应的格式即可。
始终以UTC格式存储时间数据,可以做到让服务器提供统一的时间格式,特别是在存在不同时区用户的情况下。存储用户时区格式的时间数据,容易导致数据库中出现不可比较的时间,而使用服务器默认时区的时间则会因服务器位置差异导致不同的结果,更糟的是,如果涉及不同时区的多个服务器,还会产生不一样的数据。所以应确保所有的时间都以UTC格式存储在数据库中。
另一个例子是,如果数据库中涉及价格信息,最好以分为单位存储,以免出现浮点数字,呈现数据时转换成元和分的形式即可。
举个例子,这意味着99.95美元的价格将被存储为整数9995。处理浮点运算会给价格字段的数值带来问题,价格可以先转换成分,以便于处理。
有时受某种原因影响,需要用不同的格式存储数据才有更好的效果,此时内部的数据表示无须遵循同样的约定。
❍ 与此同时,最好以自然的方式表示数据 。典型的例子是,过度使用数字ID来表示有自然键的行,或者使用枚举型数据(将smallint用于表示选项列表)而不是使用短字符串。虽然这些做法在以前是有意义的,因为那时计算机设备的存储容量和处理能力非常有限,但现在这些做法带来的性能提高可以忽略不计,而且在开发时以可理解的方式存储数据会有很大帮助。
例如,与其使用一个整数字段来存储颜色,用1表示红色,2表示蓝色,3表示黄色,不如使用一个短字符串字段,即用RED、BLUE和YELLOW来表示。即使有数以百万计的记录,其存储消耗的差异也可以忽略不计,而且浏览数据时可读性会更好。
我们将在后文中看到与这个概念有关的规范化做法和去规范化做法。
❍ 任何设计都不会是完美的或完备的 。在一个处于开发阶段的系统中,模式经常需要调整。这是很正常且在意料之中的事情,应当接受这种情况。完美是优秀的敌人。在满足系统当前需要的前提下,设计应当越简单越好。过度设计,试图满足每一个未来可能的需求并导致设计复杂化,才是真正的问题所在,这样可能会导致为那些从未实现的需求打基础而浪费过多精力。请保持你的设计简洁而灵活。
众所周知,在关系数据库中,外键是很关键的一个概念。存储在表中的数据,可以与另一个表中的数据关联。这种数据分离方式意味着一组有限的数据可以不集中存储在一个表中,而是分成两个表存放。
例如,让我们看看下面这个表,开始时字段House是一个字符串。
Characters
为了确保数据的一致性和正确性,可以将House这个字段规范化。这意味着将它存储到一个不同的表中,并以如下方式强制执行一个FOREIGN KEY(外键,简称FK)约束。
Characters
Houses
这种操作方式能使数据规范化。除非首先在Houses(家族)表中引入对应的新条目,否则无法在Characters(人物)表中添加新家族成员的条目。同样,当Characters表的记录包含某个家族的引用时,Houses表的对应条目也不能被删除。这样就能确保数据的一致性,并且不会出现任何问题,例如,录入新记录时出现House Lanister(漏了一个字符n)这样的错误,这会给以后的数据查询带来麻烦。
这样做还有一个好处,就是能够为Houses表中的每个条目添加额外的信息。本例中,我们给Houses表添加了Words(格言)字段。数据也更紧凑,因为重复的信息被存储在单个记录中。
另外,这种方式也带来几个问题。首先,所有想要了解人物的家族信息的操作,都需要执行JOIN(连接)查询。在第一种模式的人物表中,我们可以通过以下SQL语句得到所需的结果:
而在第二种模式的人物表中,得使用这条语句:
这个查询需要更长的时间才能完成,因为信息需要从两个表中组合起来。对于很大的表来说,这个时间可能会很漫长。有时可能要从不同的表中进行JOIN操作,例如,当我们为每个人物添加一个PreferredWeapon(首选武器)字段和一个Weapons(武器)规范化表时。也许随着任务表字段的增加,还会添加更多的表。
数据的插入和删除操作所需时间也会更久,因为需要进行更多的检查。一般的数据操作也会花费更长的时间。
第二个问题是,规范化的数据很难进行分片。规范化的理念就是把每个元素都放在它自己的表中描述,并引用那里的数据,这种机制本身就很难进行分片,因为此时数据分区非常困难。
还有一个问题,就是数据库更难读取和操作。删除数据时需要以一种有序的方式进行,随着字段的增加,这种方式变得更加难以实现。此外,对于简单的操作也需要进行复杂的JOIN查询。这些查询的时间更长,产生的结果也更复杂。
这种通过数字ID外键实现数据规范化的应用非常典型,但并非唯一的实现方式。
为了提高数据库的可读性,可以使用自然键来简化数据描述方式。可以不用整数作为主键,而是采用Houses表中的Name字段。
Characters
Houses
这样不仅在Houses表中省去了额外的ID字段,而且还实现了基于描述性的值的引用。在数据被规范化的同时,现在恢复了原来的查询操作方式。
正如前文所述,存储字符串而非整数,所需增加的空间是可以忽略不计的。有些开发者非常抵制自然键,喜欢使用整数值,但事实上,现在从技术上并没有具备充足说服力的原因去限制字符串的使用。
只有当我们想获得Words字段中的信息时,才需要执行JOIN查询:
总而言之,这种技巧可能无法避免在平常的操作中使用JOIN查询。也许会有很多引用,导致系统在执行查询的时间上会长一些。在这种情况下,可能有必要减少对JOIN表的需求。
去规范化是与规范化相反的操作。规范化将数据分成不同的表,以确保所有的数据都是一致的,而去规范化则是将信息重新组合到一个表中,以避免表连接操作。
继续上面的例子,现在想替换这样的一个JOIN查询:
它基于以下模式。
Characters
Houses
针对单个表查询,使用以下SQL语句:
要实现去规范化,需要将数据重新组织到单个表中。
Characters
请注意,这里的信息存在重复。每个人物都有一份家族格言的副本,这种重复在数据去规范化之前是不存在的。这意味着去规范化会用到更多的空间,如果是一个有很多行的大表,所需空间要大得多。
去规范化也增加了数据不一致的风险,因为没有任何机制可以确保新记录中不会出现与旧记录相关的拼写错误,或者因误操作导致添加了非正常的家族信息。
但是,另一方面,我们现在不必再去做连接表的操作了。对于大表来说,这样可以显著提升处理速度,无论是读操作还是写操作。同时还消除了分片的顾虑,因为现在表可以在任意合适的分片键上进行分区,且分片后的表包含了所有信息。
对于通常属于NoSQL数据库的应用场景来说,去规范化是一个极为常见的做法,它删除了执行JOIN查询的功能。例如,文档数据库将数据作为子字段嵌入一个更大的实体中。虽然去规范化也有缺点,但在某些业务场景中,做这样的取舍和权衡是很有意义的。