老的、好的数据库设计

2020-09-15 02:28:37

应用程序,甚至数据库来来去去,但数据是故事中最重要的部分。通常,数据是系统存在的首要目的。这就是为什么我们不应将数据库系统视为保存数据的黑匣子,还应将其视为验证和防范数据损坏的工具。

这是在健壮和深思熟虑的数据库设计的帮助下实现的。当然,业务逻辑是在应用层编码的,它确保数据在到达数据库之前采用正确的格式。但是,谁能保证网络故障或错误不会允许损坏的“客户”呢?此外,应用层并不是通往数据库的唯一“大门”。我们可以让导入脚本、维护脚本以及普通的DBA和开发人员与之交互。在非常低的水平上采取预防措施可以确保我们的数据在存储之前总是经过检查。

拥有健壮、可靠的数据也有助于开发和测试。将列设置为Not Null可以排除许多假设该列为空的测试场景,还可以简化代码,使开发人员可以在每次访问值之前[几乎]检查它。

在强调了良好的数据库设计的重要性之后,让我们来看看我们可以使用的工具来实现这一点。

这无疑是好设计的第一条规则。我们不打算在这里深入研究规范化规则,只想强调它的重要性。关于这个主题的一篇好文章可以在这篇文章中找到。

为属性定义适当的类型是另一件需要注意的事情。这不仅可以提高数据库的性能,还可以在存储数据之前对其进行验证。因此,我们应该将数字数据保存在“整型”、“数值”字段中。“时间戳”、“时间戳”字段中的时间戳。“bit”、“char(1)”或“boolean”(RDBMS支持的任何一个)字段中的布尔值等。

日期值得一提的是。如果日期属性应该只包含日期部分(OrderDate、ReleaseDate),请使用不包含时间部分(“Date”)的类型。如果只需要保留时间(StartTime、EndTime),请使用合适的时间类型。如果不需要精度,请将其指定为零(“time(0)”)。具有时间部分的日期的问题在于,当您在与数据库不同的时区格式化日期时,您总是必须切断时间部分以仅显示日期,并确保不显示昨天或明天。带有时间部分的日期在跳过日光更改日期时也可能会导致减法和加法问题。

约束是我们今天讨论的主要话题。它们可以防止无效数据进入并确保其健壮性。让我们逐一来看一看。

如果业务规则规定属性应始终存在,请毫不犹豫地使其不为Null。较好的非Null候选字段有ID、Name、AddedDate、IsActive、State、CategoryID(如果所有项目都应该有类别)、ItemCount、Price和许多其他字段。通常,这些属性在业务逻辑中扮演重要角色。其他可选的信息性字段可能仍为空。

但请注意,对于可以为Null的属性,不要过度使用Not Null约束。例如,长期运行的任务始终具有StartTimestamp(非Null),但EndTimestamp仅在任务完成(Null)时更新。或者另一个经典示例:Employee表有ManagerId,但并非所有员工都有经理。不要试图将ManagerId设为非Null,并为没有经理的员工插入“0”或“-1”。当我们添加外键约束时,这将导致其他问题。

同样,根据业务规则,某些属性(或属性组合)应该是唯一的,如ID、PinNumber、BookId和AuthorId、OrderNo等。这些属性应该通过添加UNIQUE约束来使其唯一。另请注意:您可以使用唯一索引来达到相同的效果,但添加约束是更好的方法。因为当您添加UNIQUE约束时,会自动创建非UNIQUE索引。因此,如果出于某种原因,您必须临时禁用/启用约束,这将非常容易。在使用唯一索引的情况下,您必须删除/重新创建索引,这在性能和时间方面是一项代价高昂的操作。

Not Null和Unique约束一起构成主键。当我们想到主键时,像ID或ObjectID这样的列很快就会出现在我们的脑海中。但是主键也可以是复合的,比如BookId和AuthorId。这里的两难境地是将单独的ID列作为主键,还是将两者组合成一个复合主键?拥有单独的ID列通常是一种更好的方法,因为它使您的连接更简洁,并且还允许轻松地向唯一组合中添加另一列。但是,拥有单独的主键(ID)并不意味着我们不需要向BookId和AuthorId列添加唯一约束。

CHECK约束允许我们定义数据的有效值/范围。检查约束的理想候选者是Percent(介于0和100之间)、State(0,1,2)、Price、Amount、Total(大于或等于0)、PinNumber(固定长度)等。同样,不要尝试将业务逻辑编码到检查约束中。我记得有一个案例,当向AccountBalance列添加“大于或等于零”的检查约束时,可以避免意外透支余额。

默认约束也很重要。它们允许我们将新的not Null列添加到现有表中,并使“旧的”API与新结构兼容,直到所有各方都升级为止(尽管在完全升级之后应该删除缺省约束)。

这里有一件事要记住。不要将业务逻辑编码到默认约束中。例如,函数“NOW()”非常适合(但并非总是)作为日志表中时间戳字段的默认值,但不适合作为ORDERS表中OrderDate字段的默认值。您可能会倾向于在插入中省略OrderDate,这依赖于缺省约束,这意味着将业务逻辑向下扩展到数据库级别。此外,在某些情况下,业务可能只在OrderDate获得批准后才决定分配它,并且因为默认约束深埋在数据库中,所以当我们在应用层更改代码时,它将不会很明显。

外键约束是关系数据库设计的王道。外键与主键一起确保表间级别的数据一致性。规范化规则告诉我们何时将数据提取到其表中,并使用外键引用它。在这里,我们将关注细微的差别,比如OnDelete和OnUpdate规则。

让我们从最简单的部分开始:OnUpdate。外键引用主键,它们很少更改(如果有的话)。因此,OnUpdate规则不是很流行,但将其设置为Cascade是有意义的,因为有时我们可能需要更新某些行的主键(通常在迁移之后)。这样,数据库将允许我们进行更新,并且新的ID将被传播到子表。

OnDelete规则稍微复杂一些。根据数据库的不同,我们有选项NoAction、Restricte、SetNull、SetDefault和Cascade。那么该选哪一个呢?

通常为引用查找或实体的键选择NoAction,这些查找或实体可以在没有引用实体的情况下存在。例如,Products>;Categories、Books>;Authors等。大多数情况下,限制与NoAction相同,但对于某些数据库则有细微差别。

另一方面,当子记录没有其父记录就不能存在时,会选择级联。在Book and Author示例中,当图书被删除时,我们也应该从BookAuthor表中删除记录。其他示例:OrderDetails->;Orders、PostComments->;Posts等。在这里,您中的一些人可能不同意数据库不应该自动删除子行,而应该由应用层删除它们。根据业务逻辑,是的,这是正确的。但有时“不重要”的子删除可以委托给数据库。

很少使用SetNull。例如,Employee.ManagerId和Employee.Id之间的外键可以设置为Null。一旦经理被免职(如果有的话),他的下属就成了孤儿。显然,仅当该列可为空时才应选择此规则。

SetDefault在他的同行中是最罕见的。删除父记录时,它会将该列设置为其默认值。因为外键引用主键,所以我们很难想象具有外键的字段有一个硬编码的默认值。但不管怎样,这个选项是存在的,以防我们需要它。

索引是好的数据库设计的重要部分,但在我们的讨论中有点离题,因为它们对保护我们的数据几乎没有什么作用(唯一索引除外)。需要注意的一点是:一些RDBMS系统(例如Oracle)会自动创建一个关于外键创建的索引,我们不必担心这一点。其他(例如MS SQL Server)则不会,我们必须自己添加索引。

深思熟虑的设计可以为我们节省大量的编码、测试和故障排除时间。针对设计良好的数据库编写查询和报告是一种乐趣。将数据发布和迁移到新系统也非常容易。

本文中提出的观点是多年处理各种数据库系统的结果。有些可能适合你的情况,有些甚至相反。所以不要认为它们是理所当然的。