引言
将表称为关系型数据库的“基本构成块”有些简化了。虽然在列出你感兴趣的每个数据元,并将其声明为整数、日期等时,很难出现太大偏差,但有效的关系型数据库设计首先取决于经过深思熟虑和良好制定的表设计。表必须对用户和开发者来说清晰易读,其模式能够合理解释它们所代表的信息。尽管数据库会尽力无监督地处理物理存储,但理解此过程的工作方式并针对其进行设计,对于包含许多大值的“宽表”记录来说尤为重要。
一个表通常(但并非总是)将单个概念的各个方面分组:一个人的姓名和密码、酒店预订的入住和退房日期,或货物的来源、目的地、状态和追踪号。决定一个表何时结束、另一个表何时开始的艺术和科学称为范式化(Normalization),我们将在后续章节中介绍。
无效的表设计有多种形式。它们可能未能捕捉到表所代表主题的重要特征,对
命名
关于如何处理数据库中命名的具体细节的观点不胜枚举,众说纷纭。没有人能在所有方面都达成一致,一个人对单数或复数表名(或者,就此而言,关键字大写或小写)的偏好很大程度上是习惯使然。RDBMS 类型也扮演着角色,PostgreSQL 倾向于使用 snake_case
命名,而 UpperCamelCase
则是 SQL Server 的标志。
就我个人而言,我使用 snake_case
,布尔值前缀为 is_
,日期后缀为 _at
,表名用复数,列名用单数,连接表命名为 leader_followers
。但正如任何值得倾听的指南都会告诉你的:最重要的一点不是你是否照我说的做,而是你是否保持一致,无论是与你自己的偏好一致,还是与你正在使用的现有数据库的约定一致。
存储:将记录转化为二进制数据
现代关系型数据库通常会抽象化数据如何排列和存储的大部分复杂细节。存储的基本单位是页面(page),它是大小统一的记录和元数据逻辑块,对应于物理存储空间的一个段:Oracle 中为 2kb,DB2 中为 4kb,SQL Server 和 PostgreSQL 中为 8kb,MySQL 的 InnoDB 中为 16kb。当数据库读写数据时,它是逐页而非逐记录进行的。
一个
所有这些技术都会产生性能开销,因为 SQL 语句读取或写入的页面越少,或跟随的指针越少,速度就越快。但页面分配效率低下还有另一种方式会损害性能:当表的大多数或所有元组只占用页面一半多一点的空间时,剩余空间就会被闲置。因为它仍然被分配着,这会膨胀表的磁盘使用量,并且一个检索或影响 n 行的查询必须加载所有 n 个页面。
大多数企业级关系型数据库,Postgres 除外,还会以区段(extents)为组管理页面,并将区段又分组在段(segments)之下。这种更复杂的策略有助于保持行的顺序,从而可以加速搜索。Postgres 在这方面也有自己的技巧,例如使用可见性映射来支持“仅索引查询”,完全跳过查看表的页面。索引本身也存储在页面中,尽管它们有自己的组织方式。
Cassandra 和 HBase 等列式数据库的主要创新在于放弃了逐元组的物理页面布局,以加快逐列检索大量数据的速度。然而,这绝不是免费午餐,因为许多基本的 RDBMS 功能本身就依赖于元组。
数据类型
类型对于关系型数据库具有多重目的。如同其他任何地方的类型一样,它们建立了一个契约:books.title
是文本并且始终是文本,recipients.postcode
是数字并且始终是数字——后一个假设在收件人居住在英国、加拿大或任何其他几个国家(邮政编码实际上是文本)时就会失效。
但对关系型数据库同样重要的是,类型定义了大小,从而决定了页面布局。SQL 标准将整数定义为四个字节,无论这四个字节代表未确定值(NULL
)、零还是二十亿。声明为固定长度 n 的 CHAR
文本总是占用 n 字节,较短的值会用空格填充,直到达到所需长度。有时,空格甚至会在 SELECT
语句中返回,例如在 SQL Server 中。
然而,并非所有类型都定义固定长度。标准 VARCHAR
类型定义的是最大长度,并且值不会像 CHAR
那样进行填充。变长类型意味着变长元组,这反过来让数据库在将这些元组放入页面时具有更大的灵活性。
总的来说,SQL 标准定义了多种类型,分为数字、布尔、日期和时间、时间间隔、文本、二进制,一直到 XML 和 JSON。对标准的忠实遵循程度,一如既往地,在不同实现中各不相同。有些在此基础上添加了非标准数据类型,货币、几何和地理、范围等类型也相继出现。
选择数据类型
为数据元定义合适的类型通常很简单。如果你使用 PostgreSQL,关心精确度,并且值精确到小数点后八位,那就可以排除浮点类型,并表明你需要 `NUMERIC` 的精度。这类问题中的大多数其他问题也都类似地解决。
主要复杂性在于文本。ISO 标准变长字符类型 VARCHAR
或 SQL Server 中支持 Unicode 的 NVARCHAR
适用于大多数用途,固定大小的 CHAR
被降级用于特定固定长度字符串的情况;更难的决定是合适的长度。有时你会很幸运,短信众所周知的外部强制限制让你可以通过将帖子限制在140字节来蒙混过关,让人们习惯它。但即使 Twitter 坚持“微博”中的“微”字最终也有所放松,与此同时,一个20字节的 city_name
字段几乎是在“恳求”来自 Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch 的人出现。
空间便宜,VARCHAR
不计算未使用的空间,而且大多数表甚至不太可能接近每页一个元组。通常不值得为长度的每一个字节斤斤计较,而是选择一个好的整数,例如比最合理的最大值高一个数量级(这里是100)或近似2的幂(255在旧的数据库软件中也是一个内置限制)。
PostgreSQL 是一个特例:不建议使用 CHAR(n)
和 VARCHAR(n)
,除非明确需要长度限制。相反,非标准 TEXT
类型,由 varlena (可变长度数组) 数据结构支持,提供无限制的文本存储。
大多数关系型数据库中的“大对象”类型是一类二进制和文本数据类型,适用于图像和文档等值,这些值通常足够长,使得页内存储不切实际。读写大对象存储是在读写页面之外的一个额外步骤,因此对于名称、序列号和摘要等规模的数据,内联存储仍然是更优选择。二进制类型在大多数关系型数据库中统称为 BLOB
(二进制大对象),尽管 Postgres 唯一支持 varlena 的二进制类型是 BYTEA
。大型文本类型可能是 TEXT
或 CLOB
,或在 SQL Server 中为 VARCHAR(MAX)
。
这里值得一提的是,关系型数据库在作为文件服务器方面表现不佳。图像、音频或视频文件、文本文档等通常最好存储在专门设计用于存储和服务的系统中。但是,如果这些数据,就数据库而言是无结构化的,但与特定记录直接相关——比如为机械部件在制造过程中跟踪而记录的二进制测试输出文件,或者被抓取和分析的网页源代码——那么大对象类型就足够了。
半结构化数据类型
在关系型数据库的大部分历史中,子字符串等一直是深入研究大对象的限制,而大对象在结构上是透明的。像 JSON
和 XML
这样的数据类型提出的问题是:“如果它们不是这样呢?”。JSON 和 XML 文档都是分层的,早期的数据库设计者必须使用外键和 JOIN
来适应。此外,半结构化类型描述自己的模式。数据库唯一能强制的要求是值必须格式正确,无论其内容如何。
层次结构并不少见,即使在主要通过关系规则运行的信息系统中也是如此。可寻址、可处理的层次结构作为数据类型,使得存储和检索尽可能简单,并能有效融合关系型和文档型策略。一个几乎只封装文档字段的表甚至可以在定义模式的早期阶段成为一个有用的原型工具。虽然 SQL 不太可能与例如 JavaScript 对以其命名的格式的易用性相匹配,但围绕这些类型(分层或其他)存在的功能为数据库编程开辟了许多可能性。
集合及更多
有些值只有组合在一起才有意义:下限和上限、开始日期和结束日期、有序的数字或字符串列表。范围可以通过分解为例如 started_at
和 ended_at
字段来模拟,而在严格的关系型术语中,数组通常被规范化为一个带有外键的单独表,但将范围或数组视为单个值来测试包含、重叠和其他特殊操作的便利性不容小觑。范围类型目前是 PostgreSQL 独有,而 Postgres 和 Oracle 都支持数组。
另一种常见情况是列表示一组定义明确且相对静态的值之一:状态码、大洲等。CHAR
和 VARCHAR
以这种方式使用会占用大量冗余空间,并且必须仔细检查和约束,以防止无意义的值进入;INT
代码更安全,但总是需要查询。许多编程语言以枚举(enum
)的形式为数字代码提供可读名称。PostgreSQL 和 MySQL 都支持这些作为列数据类型,尽管方式略有不同,MySQL 的枚举操作类似于对单个列的约束,而 Postgres 的枚举是可重用的。
自定义类型
SQL Server 或 Oracle 中的用户定义类型,PostgreSQL 中的复合类型:它们确实存在。例如,POINT
类型可以确保其 x 和 y 值不可分离,而元组中嵌套元组的可能性会迅速变得更加复杂。处理自定义数据类型并不总是容易,尤其是在数据库外部,因此通常最好根据内置或扩展提供的类型来定义数据模型。
不止是完整
你感兴趣的每个数据元列表,这个是整数,那个是日期,依此类推。类型和元组、列和行使表在功能上完整。但除了完整性,还有更多需要考虑:一个表如何确保它记录的信息是正确的?
在接下来的《正确性与约束》中,我们将介绍关系型数据库用于在数据存储的各个层面定义和强制执行正确性的工具。