写点什么

怎样实现良好的数据库设计?

2020 年 10 月 10 日

怎样实现良好的数据库设计?

本文最初发布于 relinx.io 博客,经原作者授权由 InfoQ 中文站翻译并分享。


为什么要关注数据库设计?


无论是应用程序,还是数据库如何变化,数据始终是最重要的部分。通常,数据是系统存在的首要目的。这就是为什么,我们不应该只把数据库系统看作是保存数据的黑盒子,而要将其看成验证和防止数据腐化的工具。


要做到这一点,就要有健壮和深思熟虑的数据库设计。当然,业务逻辑是在应用层编码,它确保数据在到达数据库之前的格式是正确的。


但是,谁能保证网络故障或缺陷不会放行不可靠的“客人”?此外,应用层并不是通往数据库的唯一的“门”。我们可以使用导入脚本、维护脚本,DBA 和开发人员也会与之交互。我们可以在底层采取预防措施确保在数据存储前总是进行检查。


拥有健壮、可靠的数据也有助于开发和测试。将一个列设置为 Not Null 可以省掉许多假设该列为空的测试场景,还能简化代码,让开发人员不用(几乎)每次访问它之前都检查值。


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


规范化



这无疑是良好设计的首要原则。这里,我们不打算深入研究规范化规则,只是想强调它的重要性。


关于这个话题,这里有份不错的资料,你可以进一步阅读。


数据类型


另一件要注意的事情是定义适当的属性类型。这不仅可以提高数据库的性能,还能在存储数据前验证数据。所以,我们应该在“integer”、“numeric”字段中保存数值数据;在“timestamp”、“timestamptz”字段中保存时间戳;在“bit”、“char(1)”或“boolean”字段中保存布尔值等等。


日期值得特别注意。如果 Date 属性假设只有日期部分(OrderDate,ReleaseDate),请使用没有时间部分的 Date 类型。如果你只需要保留时间(StartTime,EndTime),就使用合适的时间类型。


如果不需要指定精度,则将其指定为零(“time(0)”)。对带有时间部分的日期,有一个问题是,你必须总是截断时间部分,只显示日期,并且当你要在与数据库所在时区不同的地方显示时,要确保格式化后不会显示成昨天或明天。当跳转到夏令时的时候,带有时间部分的日期时间加减也可能出现问题。


约束


约束是本文讨论的重点。它们将无效数据排除在外,并确保数据的健壮性。让我们一个一个来看。


非空约束


如果业务规则要求该属性应该始终存在,那么要毫不犹豫地将其设置为 Not Null。适合设置为 Not Null 的字段有 Id、Name、AddedDate、IsActive、State、CategoryId(如果所有项都应该有一个类别)、ItemCount、Price 以及许多其他字段。通常,这些属性在业务逻辑中扮演重要角色。其他可选的信息字段可能还是可以设置为 Null。


但是要注意,不要对可以为空的属性使用 Not Null 约束。例如,一个长时间运行的任务总有一个 StartTimestamp(Not Null),但是只有在任务完成时才更新 EndTimestamp(Null)。


另一个典型的例子是,Employee 表的 ManagerId,并不是所有员工都有经理。不要试图让 ManagerId 不为空,并为没有经理的员工插入“0”或“-1”。当我们添加外键约束时,这将导致其他问题。


唯一约束


同样,根据业务规则,一些属性(或属性的组合)应该是惟一的,比如 Id、PinNumber、BookId 和 AuthorId、OrderNo 等。应该通过添加惟一约束来保证这些属性的惟一。


还有一点要注意:可以使用唯一索引来实现同样的效果,但是添加约束是更好的方法。因为当添加惟一约束时,会自动创建非惟一索引。


因此,如果出于某种原因,你必须临时禁用/启用约束,将会非常容易。在使用唯一索引的情况下,你必须删除/重新创建索引,从性能和时间方面来说,这是一个昂贵的操作。


主键


Not Null 和唯一约束一起构成主键。当我们想到主键时,会很快想到 Id 或 ObjectId 之类的列。但是主键也可以是复合的,比如 BookId 和 AuthorId。


这里有个难题是,是使用单独的 Id 列作为主键,还是将两者的组合作为主键?通常,使用单独的 Id 列是一种更好的方法,因为它可以使连接更加清晰,还能方便地将另一列添加到惟一组合中。但是,即使有了一个单独的主键(Id),我们还是要为 BookId 和 AuthorId 列添加唯一约束。


Check 约束


Check 约束允许我们定义数据的有效值/范围。适合 Check 约束的属性有百分比(0 到 100 之间)、状态(0、1、2)、价格、金额、总数(大于或等于 0)、PinNumber(固定长度)等。


同样,不要尝试将业务逻辑编码到 Check 约束中。我记得有一次,在 AccountBalance 列中添加了一个“大于或等于零”的 Check 约束,从而避免了意外透支。


默认约束


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


这里要记住一点。不要在默认约束中编写业务逻辑。例如,函数“now()”可能很适合(尽管不总是)作为日志表中的时间戳字段的默认值,但不适合 Orders 表的 OrderDate 字段。你可能会倾向于在插入语句中省略 OrderDate,而依赖于默认约束,但这意味着将业务逻辑扩展到数据库层。


此外,在某些情况下,业务可能只在订单批准后才给 OrderDate 赋值,因为默认约束深埋在数据库中,所以,当我们对应用层的代码进行更改时,它不会那么明显。


外键约束


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



DBeaver 中的外键约束编辑器


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


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


通常,对于键引用查找或不引用实体的实体,我们选择 NoAction。例如,Products -> Categories、Books -> Authors 等。在大多数情况下,Restrict 与 NoAction 是相同的,但是对于某些数据库,它们有细微的区别


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


SetNull 很少使用。例如,Employee.ManagerId 和 Employee.Id 之间的外键可以是 SetNull。当一名经理被撤职,他的下属就没经理了。显然,只有当列可为空时才能选择该项规则。


在这些规则中,SetDefault 最罕见。当父记录被删除时,它将列设置为其默认值。因为外键引用主键,我们很难想象一个有外键的字段将默认值硬编码。但无论如何,这个选项是存在的,我们还是有可能需要它。


索引


索引是良好数据库设计的重要组成部分,但有点偏离我们的讨论,因为它们几乎不能保护我们的数据(惟一索引除外)。需要注意的一点是:一些 RDBMS 系统(例如 Oracle)会在创建外键时自动创建索引,而无需我们操心。其他数据库(例如 MS SQL Server)不会这样做,我们必须自己添加索引。


小结


一个深思熟虑的设计可以为我们节省大量的编码、测试和故障排除时间。在设计良好的数据库上编写查询和报表令人愉快。将数据发布并迁移到新系统也会非常容易。


编码快乐!


原文链接:


https://relinx.io/2020/09/14/old-good-database-design/


2020 年 10 月 10 日 16:412831
用户头像

发布了 328 篇内容, 共 142.2 次阅读, 收获喜欢 681 次。

关注

评论 1 条评论

发布
用户头像
外键约束一般都强制不适用。。。
2020 年 10 月 15 日 17:58
回复
没有更多了
发现更多内容

深入理解Git的实现原理

程序员小灰

c++ git Linux 项目管理 架构师

网络入门模拟器:Cisco Packet Tracer

网络技术平台

史上最优美的Android原生UI框架XUI使用指南

android UI 框架开发

面试被问线程安全怎么保障,我的回答让面试官眼前一亮

996小迁

Java 学习 架构 面试 笔记

我是怎么教我6岁女儿编程的

勇往直前的胖子

少儿编程

架构师视角 | 分布式缓存如何选择 ?

Java架构师迁哥

roblox 杂记

katichar

线上数据被回滚两次我都做了哪些不正确的操作

新世界杂货铺

MySQL 后端

LeetCode题解:45. 跳跃游戏 II,贪心正向查找,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

让战略不再”空虚“的战略描述

Alan

战略思考 战略

移动端技术方案设计的经验总结

张明云

android 架构 移动应用 架构师 技术方案

架构师训练营第 12 周总结

邓昀垚

培训是为了激发学员学习这门课的兴趣

boshi

职业 培训

传销组织的CTO | 法庭上的CTO(4)

赵新龙

CTO 传销 法庭上的CTO

一文搞懂RESTful API

bigsai

RESTful Rest

史上最实用的Android切片应用库XAOP使用指南

android aop 开源项目 框架

架构师训练营第12周作业

邓昀垚

程序员有必要读研吗?

Java架构师迁哥

话题讨论 | 那些年奇葩的面试经历

三号无名指

话题讨论

史上最好用的Android全量版本更新库XUpdate使用指南

android UI 框架开发 xupdate

架构词典:SLA

lidaobing

架构 SLA

二、关于大型复杂系统

数列科技杨德华

甲方日常 63

句子

工作 随笔杂谈 日常

《程序员数学:使用Python进行3D图形,机器学习和仿真》PDF免费下载

计算机与AI

Python 学习 数学

计算机网络基础

Minar Kotonoha

node.js 前端 计算机网络 HTTP

史上最全的开源项目创作指南

开源 经验分享

default-servlet-handler不生效原因,springmvc静态资源拦截方案比较

叫练

springmvc 静态资源拦截 default-servlet-handler 资源配置不生效

Flutter Plugin插件开发填坑指南

flutter 经验分享

技术博客,从零到数万访问,这两年我都做了什么

android 博客 经验分享

Redis 为什么这么快?这才是最完美的回答

Java架构师迁哥

架构师训练营第十一周命题作业

一马行千里

极客大学架构师训练营 命题作业

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

怎样实现良好的数据库设计?-InfoQ