写点什么

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

  • 2020-10-10
  • 本文字数:3069 字

    阅读完需:约 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:416470
用户头像

发布了 689 篇内容, 共 399.1 次阅读, 收获喜欢 1498 次。

关注

评论 1 条评论

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

小白也能看懂的锁升级过程和锁状态

牧小农

乐观锁 悲观锁 锁升级

隐式等待、显示等待和强制等待?

Geek_6370d5

华为 IS-IS防环、泄露

艺博东

网络

音视频开发经验之路【三】吐血干货,直播首屏耗时400ms以下的优化实践

鱼哥

音视频

『BATcoder』都2021年了,是时候安装Ubuntu了!

刘望舒

android Android进阶

go-admin开源项目分析-1[安装、配置、启动]

happlyfox

学习 3月日更 Go 语言 go-admin

Python OpenCV 图像标记,取经之旅第 12 天

梦想橡皮擦

3月日更

阿里二面,面试官居然把 TCP 三次握手问的这么细致

Java 编程 架构 面试 网络

ARTS-week 4

steve_lee

MySQL主从架构原理

无心

MySQL

面试三轮我倒在了一道sql题上——sql性能优化

牧小农

MySQL 数据库 面试 索引 索引优化

Java面试:阿里公开内部P7架构师(Java)成长路线图,满满的干货

Java架构追梦

Java 阿里巴巴 架构 面试 P7岗

架构师知识笔记2

felix徐

mysql 检索分享上篇

new life

Redis - RDB

insight

redis 3月日更

音视频开发经验之路【一】Android中如何实现无缝切换播放源

鱼哥

音视频

是时候下载Android11系统源码和内核源码了~

刘望舒

android Android进阶

畅玩国服LOL?MacBook M1 Windows虚拟机体验

Zhendong

arm MacBook m1

看完这篇,再也不怕面试官问我线程池了

牧小农

多线程 高并发 线程池 阻塞队列 拒绝策略

一线大厂软件测试流程(思维导图)详解

程序员阿沐

编程 程序员 软件测试 测试工程师 测试流程

一个视频带你编译自己的Android系统!

刘望舒

android Android进阶

LeetCode链表专题01: 反转链表

小马哥

Java 链表 LeetCode 数据结构与算法 七日更

极客时间·产品训练营·第八周作业

二大爷

极客大学产品经理训练营

Spring Boot Feign 使用与源码学习

Yangjing

Feign Spring boot Feign 源码解读

音视频开发经验之路【二】ijkplayer实际开发中遇到的问题总结

鱼哥

音视频

蚂蚁开源增强版 SpringBoot,都有高级特性?

Java小咖秀

开源 springboot 蚂蚁金服 SOFA

Go语言的初体验

Phoenix

Go 语言

2021拼多多(Java岗)面经最新分享

比伯

Java 编程 架构 面试 计算机

初识Golang之条件语句

Kylin

基础语法 3月日更 Go 语言

程序思维与架构思维

Simon

架构实战营

(Day31) 变革流程的流程

mtfelix

28天写作 bewriting

怎样实现良好的数据库设计?_文化 & 方法_Elnur_InfoQ精选文章