DDIA 第一部分-3. 数据模型与查询语言

0 阅读13分钟

在复杂的应用程序中可能有更多的中间层,例如基于 API 之上的 API,但基本思想仍然相同:每一层通过提供一个简洁的数据模型来隐藏下层的复杂性。这些抽象允许不同的人群 —— 例如,数据库供应商的工程师和使用他们数据库的应用程序开发者 —— 有效地合作。

关系模型与文档模型

对象关系不匹配

大部分应用程序开发都是使用面向对象的编程语言完成的,这导致了对 SQL 数据模型的常见批评:如果数据存储在关系表中,则需要在应用程序代码中的对象和数据库的表、行、列模型之间建立一个笨拙的转换层。这种模型之间的脱节有时被称为 阻抗不匹配。

对象关系映射(ORM)

用于一对多关系的文档数据模型

关系模型的局限性

ddia_0301.png


{

    "user_id": 251,

    "first_name": "Barack",

    "last_name": "Obama",

    "headline": "Former President of the United States of America",

    "region_id": "us:91",

    "photo_url": "/p/7/000/253/05b/308dd6e.jpg",

    "positions": [

        {"job_title": "President", "organization": "United States of America"},

        {"job_title": "US Senator (D-IL)", "organization": "United States Senate"}

    ],

    "education": [

        {"school_name": "Harvard University", "start": 1988, "end": 1991},

        {"school_name": "Columbia University", "start": 1981, "end": 1983}

    ],

    "contact_info": {

        "website": "https://barackobama.com",

        "twitter": "https://twitter.com/barackobama"

    }

}

一些开发人员认为 JSON 模型减少了应用程序代码和存储层之间的阻抗不匹配。JSON 表示具有更好的 局部性。如果你想在关系示例中获取个人资料,你需要执行多个查询(通过 user_id 查询每个表)或在 users 表与其从属表之间执行混乱的多路连接。在 JSON 表示中,所有相关信息都在一个地方,使查询既更快又更简单。

从用户个人资料到用户职位、教育历史和联系信息的一对多关系暗示了数据中的树形结构,而 JSON 表示使这种树形结构变得明确。

ddia_0302.png

规范化、反规范化与连接

当你使用 ID 时,你的数据更加规范化:对人类有意义的信息(如文本 Washington, DC)只存储在一个地方,所有引用它的地方都使用 ID(它只在数据库中有意义)。当你直接存储文本时,你在使用它的每条记录中都复制了对人类有意义的信息;这种表示是 反规范化 的。

规范化的权衡

作为一般原则,规范化数据通常写入更快(因为只有一个副本),但查询更慢(因为它需要连接);反规范化数据通常读取更快(连接更少),但写入更昂贵(更多副本要更新,使用更多磁盘空间)。你可能会发现将反规范化视为派生数据的一种形式很有帮助,因为你需要设置一个过程来更新数据的冗余副本。

规范化往往更适合 OLTP 系统,其中读取和更新都需要快速;分析系统通常使用反规范化数据表现更好,因为它们批量执行更新,只读查询的性能是主要关注点。此外,在中小规模的系统中,规范化数据模型通常是最好的,因为你不必担心保持数据的多个副本相互一致,执行连接的成本是可以接受的。然而,在非常大规模的系统中,连接的成本可能会成为问题。

社交网络案例研究中的反规范化

在 “案例研究:社交网络主页时间线” 中,我们比较了规范化表示和反规范化表示(预计算的物化时间线):这里,posts 和 follows 之间的连接太昂贵了,物化时间线是该连接结果的缓存。将新帖子插入关注者时间线的扇出过程是我们保持反规范化表示一致的方式。

必须仔细考虑信息更改的频率以及读写成本。规范化和反规范化本质上并不好或坏 —— 它们只是在读写性能以及实施工作量方面的权衡。

这里博文内容,和点赞数,回复数,用户信息。 变化越快的信息烦规范化到物化视图中是没有意义的,只会增加存储成本。需要规范化的方式设计。

多对一与多对多关系

多对一和多对多关系不容易适应一个自包含的 JSON 文档;它们更适合规范化表示。多对多关系通常需要"双向"查询,反规范化的表达,会因为关系存储在两个地方,可能会相互不一致。

星型与雪花型:分析模式

模式的中心是所谓的 事实表(在此示例中,它称为 fact_sales)。事实表的每一行代表在特定时间发生的事件。

事实表中的一些列是属性,例如产品售出的价格和从供应商那里购买它的成本(允许计算利润率)。事实表中的其他列是对其他表的外键引用,称为 维度表。由于事实表中的每一行代表一个事件,维度代表事件的 谁、什么、哪里、何时、如何 和 为什么。

该名称来自这样一个事实:当表关系被可视化时,事实表位于中间,被其维度表包围;到这些表的连接就像星星的光芒。

何时使用哪种模型

文档数据模型的主要论点是模式灵活性、由于局部性而获得更好的性能,以及对于某些应用程序来说,它更接近应用程序使用的对象模型。关系模型通过为连接、多对一和多对多关系提供更好的支持来反击。

文档模型中的模式灵活性

数据库中模式的强制执行是一个有争议的话题,通常没有正确或错误的答案。

模式验证在写入时或读取时,都是看具体的使用场景的

读写的数据局部性

局部性优势仅在你同时需要文档的大部分时才适用。数据库通常需要加载整个文档,如果你只需要访问大文档的一小部分,这可能会浪费。在文档更新时,通常需要重写整个文档。由于这些原因,通常建议你保持文档相当小,并避免频繁对文档进行小的更新。

文档的查询语言

文档和关系数据库的融合

模型的这种融合对应用程序开发人员来说是个好消息,因为当你可以在同一个数据库中组合两者时,关系模型和文档模型效果最好。许多文档数据库需要对其他文档进行关系式引用,许多关系数据库也有一些场景更适合模式灵活性。关系-文档混合是一个强大的组合。

图数据模型

如果你的应用程序主要具有一对多关系(树形结构数据)并且记录之间很少有其他关系,则文档模型是合适的。

关系模型可以处理多对多关系的简单情况,但随着数据内部连接变得更加复杂,开始将数据建模为图变得更加自然。

在 邻接表 模型中,每个顶点存储其相距一条边的邻居顶点的 ID。或者,你可以使用 邻接矩阵,这是一个二维数组,其中每一行和每一列对应一个顶点,当行顶点和列顶点之间没有边时值为零,如果有边则值为一。邻接表适合图遍历,矩阵适合机器学习。

常用构建发生的事件和事实

ddia_0306.png 属性图

边表就像我们在 “多对一与多对多关系” 中看到的多对多关联表/连接表,泛化为允许在同一表中存储许多不同类型的关系。标签和属性上也可能有索引,允许有效地找到具有某些属性的顶点或边。

图适合可演化性:随着你向应用程序添加功能,图可以轻松扩展以适应应用程序数据结构的变化。

Cypher 查询语言

Cypher 是用于属性图的查询语言,最初为 Neo4j 图数据库创建,后来作为 openCypher 发展为开放标准

SQL 中的图查询

自 SQL:1999 以来,查询中可变长度遍历路径的想法可以使用称为 递归公用表表达式(WITH RECURSIVE 语法)的东西来表达。

三元组存储与 SPARQL

在三元组存储中,所有信息都以非常简单的三部分语句的形式存储:(主语、谓语、宾语)。例如,在三元组(Jimlikesbananas)中,Jim 是主语,likes 是谓语(动词),bananas 是宾语。

一些三元组存储的研究和开发工作是由 语义网 推动的,这是 2000 年代初的一项努力,旨在通过不仅以人类可读的网页形式发布数据,还以标准化的机器可读格式发布数据来促进互联网范围的数据交换。尽管最初设想的语义网没有成功,但语义网项目的遗产在几项特定技术中继续存在:链接数据 标准(如 JSON-LD )、生物医学科学中使用的 本体、Facebook 的开放图协议(用于链接展开)、知识图(如 Wikidata)以及由 schema.org 维护的结构化数据的标准化词汇表。

会不会成为AI时代的交互语言

RDF 数据模型

RDF 有一些怪癖,因为它是为互联网范围的数据交换而设计的。三元组的主语、谓语和宾语通常是 URI。

SPARQL 查询语言

SPARQL 是使用 RDF 数据模型的三元组存储的查询语

Datalog:递归关系查询

Datalog 是一种比 SPARQL 或 Cypher 更古老的语言:它源于 20 世纪 80 年代的学术研

GraphQL

GraphQL 是一种查询语言,从设计上讲,它比我们在本章中看到的其他查询语言限制性更强。GraphQL 的灵活性是有代价的。采用 GraphQL 的组织通常需要工具将 GraphQL 查询转换为对内部服务的请求,这些服务通常使用 REST 或 gRPC。

响应是一个反映查询结构的 JSON 文档:它正好包含请求的那些属性,不多也不少。这种方法的优点是服务器不需要知道客户端需要哪些属性来渲染用户界面;相反,客户端可以简单地请求它需要的内容。

事件溯源与 CQRS

在我们迄今为止讨论的所有数据模型中,数据以与写入相同的形式被查询 —— 无论是 JSON 文档、表中的行,还是图中的顶点和边。然而,在复杂的应用程序中,有时很难找到一种能够满足所有不同查询和呈现数据方式的单一数据表示。在这种情况下,以一种形式写入数据,然后从中派生出针对不同类型读取优化的多种表示形式可能是有益的。

也许写入数据的最简单、最快速和最具表现力的方式是 事件日志:每次你想写入一些数据时,你将其编码为自包含的字符串(可能是 JSON),包括时间戳,然后将其追加到事件序列中。此日志中的事件是 不可变的:你永远不会更改或删除它们,你只会向日志追加更多事件(这可能会取代早期事件)。事件可以包含任意属性。

使用不可变事件日志作为真相来源(权威数据源),并从中派生物化视图。

使用事件作为真相来源(权威数据源),并将每个状态变化表达为事件的想法被称为 事件溯源。

维护单独的读优化表示并从写优化表示派生它们的原则称为 命令查询责任分离(CQRS)。

当用户的请求进来时,它被称为 命令,首先需要验证。只有在命令已执行并确定有效后,它才成为事实,相应的事件被添加到日志中。在以事件溯源风格建模数据时,建议你使用过去时态命名事件(例如,“座位已预订”),因为事件是记录过去发生的事情的记录。即使用户后来决定更改或取消,他们以前持有预订的事实仍然是真实的,更改或取消是稍后添加的单独事件。

事件溯源与星型模式事实表之间的相似之处是两者都是过去发生的事件的集合。然而,事实表中的行都具有相同的列集,而在事件溯源中可能有许多不同的事件类型,每种都有不同的属性。此外,事实表是无序集合,而在事件溯源中事件的顺序很重要:如果先进行预订然后取消,以错误的顺序处理这些事件将没有意义。

优点,缺点

你可以在任何数据库之上实现事件溯源,但也有一些专门设计来支持这种模式的系统,例如 EventStoreDB、MartenDB(基于 PostgreSQL)和 Axon Framework。

数据框、矩阵与数组

还有一些数据模型你可能会在分析或科学环境中遇到,但很少出现在 OLTP 系统中:数据框和多维数字数组(如矩阵)。

乍一看,数据框类似于关系数据库中的表或电子表格。它支持对数据框内容执行批量操作的类关系算子。

数据框通常不是通过声明式查询(如 SQL)而是通过一系列修改其结构和内容的命令来操作的。这符合数据科学家的典型工作流程,他们逐步"整理"数据,使其成为能够找到他们所提问题答案的形式。

ddia_0309.png

举例

数据框足够灵活,允许数据从关系形式逐渐演变为矩阵表示,同时让数据科学家控制最适合实现数据分析或模型训练过程目标的表示。

还有像 TileDB 这样专门存储大型多维数字数组的数据库;它们被称为 数组数据库,最常用于科学数据集,如地理空间测量(规则间隔网格上的栅格数据)、医学成像或天文望远镜的观测 。数据框在金融行业也用于表示 时间序列数据,如资产价格和随时间变化的交易 。

总结

非关系数据模型的一个共同点是,它们通常不会对存储的数据强制执行模式,这可以使应用更容易适应不断变化的需求。然而,你的应用很可能仍然假设数据具有某种结构;这只是模式是显式的(在写入时强制执行)还是隐式的(在读取时假设)的问题。