作为数据库设计人员,当我们负责数据库项目时,在数据库设计以及把数据库部署到生产环境的过程中可能会遇到一些挑战。
其中一些问题不可避免,也无法控制。但是,其中相当一部分可以追溯到数据库设计本身的质量。我们在初步阶段所做的决定会对数据库最终的工作情况有深远的影响。
如果我们要建一所房子,我们不会聘请一位工程承包商,然后马上就要求他们开始打地基。这会导致灾难发生。至少,我们需要就建房计划和蓝图达成一致。数据库设计也一样。我们规划得越好,设计的输出质量就越高。
好的数据库是深思熟虑的结果,而不是临时想法的集合。糟糕的设计规划会导致结构性问题,该数据库一旦推出后,要解决这些问题是相当昂贵的。我们不可能总是能预测到数据库会遇到的所有问题,但是好的规划确保我们可以把问题减少到只有那些真正无法避免的问题。
创建数据库的目的相当广泛。从存储个人私人信息的小型数据库到处理海量信息的大规模企业数据库。设计人员必须明白数据库的目的所在,以便用最符合这些目标的方式来设计。
要问的关键问题包括:数据的性质、数据获得的方式、数据存储和检索的频率、数据的规模、使用数据的应用程序是什么。在工作日结束时手动输入数据的数据库和实时捕获并自动存储数据的复杂的行业数据库不能用同一种设计模型。
设计的关键是确保数据效率、可用性和安全性的(请参考PostgreSQL 安全)。忽略数据的目的将导致设计看上去符合所有的条条框框,但实际上是不健全的。
数据库设计不是一个严格确定的过程。两个遵循同样设计规范的开发人员最终可以设计出两个截然不同的数据库。这主要是因为任何软件工程项目都固有的创造性。尽管如此,设计的一些核心原则对确保数据库以最佳方式运行至关重要。其中之一就是规范化。规范化指的是用于把表分解成组成部分的技术。我们执行该操作,直到我们让每一张表只表示一种事物,而列描述该表所代表的项的属性。规范化是一种古老的计算概念,已经有 30 多年的历史了。事实上,SQL 主要用于读取和操作规范化数据集。为了理解规范化,有必要了解 SQL 的工作原理。
SQL 本质上是一种迭加式语言,适用于轻松创建结果集或值集。使用 FROM 子句,我们可以从一张表中提取数据,并使用 JOIN 把数据添加到另一张表的内容中。我们可以使用几乎无限数量的表来生成我们需要的数据。SQL 的迭加能力对数据库开发和性能来说都至关重要。
当索引与键值完全同步时,索引效果最佳。当我们必须使用 LIKE、CHARINDEX、SUBSTRING 及类似命令来解析值与列值的组合时,SQL 范式遭到破坏,数据可搜索性变差。
因此,规范化我们的数据库对简化开发和始终如一的高性能至关重要。尽管如此,规范化还是有很多层次的,而且存在过度规范化的数据库。良好的规范化平衡了记录插入、更新、查询和删除的需求。采用最广泛的最佳实践是,数据库必须至少规范化到第三范式(Third Normal Form,简称 3NF)。但是,第四(4NF)和第五(5NF)范式也相当有用,容易理解,也值得我们努力了解如何使用它们。
冗余表和字段对数据库设计人员和管理员来说是噩梦。它们需要占用系统资源才能保持安全、更新和备份。当我们讨论十多个记录时,冗余记录也许看起来不多。但是,在大型数据库中,冗余字段可以是数千个或数百万个,计算资源开销很大。它们不必要地增加了数据库的规模,降低了效率,增加了数据崩溃的风险。
当然,有时候冗余也许是必要的,但是,这应该是例外,而不是规则。即使允许冗余,也应当清楚地记录理由,以确保将来该理由不再有效时,数据库管理员可以删除冗余。
有时候,用户或应用程序可能需要查询一张表中的多个列。随着表中行的数量的增长,用于完成这些查询的时间也在稳步增加。为了加速查询并减少表规模的影响,谨慎的做法是索引表的列,以便在调用 SELECT 查询时,每个列中的条目几乎可以立即获得。
不幸的是,加速 SELECT 函数通常会导致更常规的 INSERT、UPDATE 及 DELETE 命令的性能恶化。这很大程度上是因为索引本身必须不断地与数据库的内容保持同步,而这又意味着大量的数据库引擎开销。因此,具有讽刺意味的是,我们加速 SELECT 查询的尝试可能导致整个数据库变慢。这是过度索引的经典案例。
对所有列只提供一个索引,并且该索引和查询表所用到的主键不同,这种方法可以解决这个问题。我们也可能按最常用到最不常用对列进行排序。索引始终是一个微妙的平衡,归根结底要用对。
包罗万象的域表不是数据库设计的最佳方法。请记住,关系数据库的构建思想:数据库中的每个对象只代表一个事物。任何数据集所代表的事物都不应该含糊不清。通过查看主键、表名、列名和关系,我们应该可以快速解读数据集的意义。尽管如此,对于数据库设计,一种挥之不去的误解是,表越多,数据库就越混乱越复杂。
通常,把几张表压缩到一张表中就是简化设计的原理。这听上去是个好主意,但是,通常得到的是效率低下且难以操作的数据库。SQL 代码将变得很长,难以阅读,也不自然。这将把两种截然不同的东西混在一起。乍一看,域表看起来像一个抽象的文本容器。从实现的角度来看,这是正确的,但是,这不是设计数据库的最好方法。
作为规范化过程的一部分,隔离和分解数据最终形成每一行只代表一个事物。每个域表与所有其他域表都不同。
多个域表的最终结果是:
数据库设计人员和开发人员常常把他们的角色完全看作是技术角色。非技术方面(如遵守命名约定)往往被推到优先级列表的较低位置,或者甚至完全被忽略。这可能是个灾难性的错误。
命名也许是设计人员自行决定的,但是,事实上,它是数据库文档的第一个也是最重要的元素(我们接下来将探讨文档错误)。数据库设计人员应该把他们的工作看作是在他们换了雇主或角色之后还将继续存在的东西。命名约定的目的是,让没有完全参与该项目的人也能比较容易地快速理解表和列的内容。未来的管理员、开发人员或用户不应当必须看完长长的文档才能理解某个表名或列名的意义。表如何命名的具体细节并未得到业界的一致同意。
最重要的是一致性。一旦我们遵循某个特定的风格来命名对象,那么在整个数据库中要坚持使用它。表名必须尽可能是表所代表的内容的完整或简约描述,而列名应该清楚地表明其所代表的信息。对于简单数据库,这并不难。但是,一旦我们构建彼此引用的表,事情就变得复杂了。严格遵循命名约定始终是正确的方向。
这样的约定包括没有列或表名的字符长度限制,以消除使用不易理解或记忆的首字母缩略词的需要。如列名 CUST_DSCR,任何人读到这个名字都将不得不猜测该列包含的内容。CUSTOMER_DESCRIPTION 则是个更好的列名,没有迫使读者展开他们的想象力。
避免冗余:在一张名为“Students(学生)”的表中,我们不必把列命标成 StudentName、StudentAddress 或 StudentGrade,因为 Name、Address 和 Grade 已经足够了。还有,不要使用保留字。用“Index”来标记某列会让人困惑,也会成为错误的来源。可以用一个描述性的前缀,如 StudentIndex。
如果数据库开发人员和设计人员在确定命名约定的优先级上碰到问题,那么他们在文档方面就会存在更大的问题。对于开发人员来说,文档有时感觉像是开发过程中一个微不足道的非必要方面。然而,很多在其他方面设计优秀的数据库已经牺牲在糟糕文档的祭坛上。糟糕的文档极大地抑制了故障排除、结构改进、升级和连续性。
数据库设计人员必须始终想象他们会在某个时刻不再参与对该数据库的支持。文档应该让其他人容易接手数据库设计、开发或管理。良好的文档必须包含列、表、关系和约束的定义,使之清楚地表明每个元素应该如何使用。如果我们可以包含示例以说明预期值,那么效果会更好。
有些设计人员会使用糟糕的文档作为确保工作安全性的一种手段,即除了他们之外,没有人能完全理解该数据库。这是一种短视和注定失败的策略,因为这几乎总是导致管理层看透设计人员的意图。糟糕的文档还让我们作为设计人员多年后返工或改进这些代码变得非常困难。
我们可以仔细地完成世界级数据库设计所要求的所有步骤。但是,如果我们没有对数据库进行严格的测试,那么我们将陷入黑暗之中。不幸的是,当项目延期时,测试阶段受到的影响最大。这是弄巧成拙,因为一个快速通过的数据库会立即被错误及不一致性所困扰,而这些错误应该很容易在测试阶段被识别和解决。
一个满是缺陷的数据库不会让用户和管理员喜欢,即使最终修复了错误,我们也摆脱不了不好的名声。在数据库上线前,进行深入而广泛地测试,这将大大减少部署到生产环境中后产生的故障的数量和规模。良好的测试不会找到每个错误,但是肯定有助于摆脱大多数错误。
数据库开发和设计是任何数据密集型项目的核心,这些项目几乎包含了所有业务应用程序。因此,设计过程应该始终在此上下文中进行审查。本文中列出的设计错误一开始会被看作是小而不起眼的问题。然而,最终,它们会极大地降低数据库性能并且修复成本高昂。要从一开始就正确地做事,增加构建非常适合其预期目的数据库的几率。