从多个表中连接数据是一个复杂的话题。主要有两种策略:数据库级别 和 应用程序级别的连接。Prisma ORM 提供了这两种选项。在本文中,您将了解这两种策略的权衡,以便为您的用例选择最佳策略。
引言
为什么 Prisma ORM 最初只提供应用程序级别的连接?
Prisma ORM 最初只提供*应用程序级别*的连接策略。做出此选择有几个原因
- 能够在不同的数据库引擎中使用相同的连接策略,确保可移植性。
- 通过将昂贵的操作移至应用程序层(应用程序层比数据库更容易、更便宜进行扩展),提高了整个系统的可伸缩性。
- 云原生和无服务器用例中,应用程序和数据库通常位于同一云区域,到数据库的额外往返开销可以忽略不计。
- 高性能用例,涉及数百万行数据和深度嵌套查询,并使用过滤和分页等额外功能。
- 查询调试简单,因为每个查询只针对一个表(无需理解和调试复杂的查询计划)。
- 通过将数据库的职责限制在简单的操作,并防止查询性能因数据库的查询规划器和运行时优化而出现显著波动,从而获得可预测的性能。
2024年2月,Prisma ORM 添加了*智能*数据库级别连接作为替代策略,利用 LATERAL
连接和 JSON 聚合等现代数据库特性。当应用程序服务器和数据库服务器距离较远,且额外网络往返的开销对查询的整体延迟贡献很大时,这种方法更为有利。
最终,这些方法各有优劣,我们将在本文的其余部分阐明这些权衡,以帮助您为您的关联查询选择最佳策略。
嵌套对象 vs 外键关系
在深入探讨连接的复杂性之前,让我们快速退一步,了解“连接数据”这个话题到底是什么。
作为一名开发者,您可能习惯于使用*嵌套*对象,它们看起来类似于这样
在此示例中,“对象层次结构”如下所示:post
→ author
→ profile
。
这种嵌套结构是大多数具有*对象*概念的编程语言中表示数据的方式。
然而,如果您之前使用过 SQL 数据库,您可能知道相关数据在那里以不同的方式表示,即以一种 *扁平* (或 *范式化*)的方式。采用这种方法,实体之间的关系通过 *外键* 表示,外键指定了表之间的*引用*。
以下是这两种方法的视觉表示
这是一个巨大的差异,不仅体现在数据在磁盘和内存中的 *物理* 布局方式,也体现在 *思维模型* 和数据推理方式上。
“连接”数据意味着什么?
连接数据的过程是指将 SQL 数据库中*扁平*布局的数据转换为应用程序开发者可以在其应用程序中使用的*嵌套*结构。
这可以在以下两个地方之一发生
- 在 数据库中:向数据库发送单个 SQL 查询。查询使用
JOIN
关键字(或可能使用关联子查询)让数据库执行跨多个表的连接,并返回嵌套结构。有多种执行此连接的方法,我们将在下一节中介绍。 - 在 应用程序中:向数据库发送多个查询。每个查询只访问一个表,然后查询结果在应用程序层中连接。
数据库级别的连接有其优点,但如果变得过于复杂,也有一些缺点。因此,根据模式、数据集和查询复杂性等因素,一种方法可能比另一种方法更适合特定的用例。请继续阅读以了解详细信息!
三种 JOIN 策略:朴素、智能和应用程序级别 JOIN
从宏观上看,有三种不同的连接策略可以应用,数据库级别的“朴素”和“智能” JOIN,以及“应用程序级别”的连接。让我们通过以下模式逐一探讨这些策略
朴素的数据库级别 JOIN 导致数据冗余
朴素的数据库级别 JOIN 指的是未采取任何额外优化措施的 JOIN 操作。由于多种原因,这类 JOIN 通常性能不佳,让我们来探讨一下!
例如,以下是开发者可能朴素地编写的一个简单的 LEFT JOIN
操作,用于连接 users
表和 post
表的数据
数据库返回的结果可能类似于这样
您注意到了什么吗?在 user_name
列的数据中存在*大量*重复。
现在,让我们将 comments
添加到查询中
现在情况更糟了!不仅 user_name
重复,post_title
也重复出现
数据的冗余带来了几个负面影响
- 增加了通过网络发送的数据量(不必要的),消耗网络带宽并增加整体查询延迟。
- 应用程序层需要做额外的工作才能获得所需的嵌套对象
- 对冗余数据进行去重
- 重新构建数据记录之间的关系
此外,这种操作会给数据库带来高 CPU 开销,因为它将查询所有三个表并执行自己的内存映射将数据连接成一个结果集。
以上仍然是一个相对简单的例子。想象一下,如果您使用更多的 JOIN 和更多的嵌套。达到一定程度后,数据库将放弃优化查询计划,而是对每个表执行全表扫描,然后使用自己的 CPU 在内存中将数据拼接起来。这会很快变得昂贵!
数据库的 CPU 和内存比应用程序级别的 CPU 和内存更复杂(也更昂贵)来扩展。因此,改善这种情况的一种方法是利用应用程序服务器的 CPU 来完成数据连接的工作,这引出了我们的下一个方法:“应用程序级别连接”。
应用程序级别连接简单高效,但有网络开销
执行这些朴素的数据库级别连接的另一种替代方案是在应用程序层连接数据。在这种情况下,开发者会构建三个不同的查询,分别发送到数据库。一旦数据库返回查询结果,开发者就可以应用自己的业务逻辑来连接数据。
在 TypeScript 中,一个示例可能如下所示(使用诸如node-postgres
之类的普通 Postgres 驱动程序)
这种方法有几个优点
- 数据库将为每个查询生成高度优化的执行计划,并且几乎不做任何 CPU 工作,因为它只是从单个表中返回数据。
- 通过网络传输的数据针对应用程序的数据需求进行了优化(并且不会像朴素的数据库级别连接策略那样遭受相同的数据冗余问题)。
- 由于大部分映射和连接工作现在都在应用程序本身完成,数据库服务器有更多资源来处理更复杂的查询。
通过将 CPU 开销从数据库转移到应用程序层,这种方法增强了整个系统的水平可伸缩性。
在 O' Reilly 的书籍高性能 MySQL中,这种应用程序级别连接的技术被称为 JOIN 分解:“许多高性能网站使用 JOIN 分解。您可以通过运行多个单表查询而不是多表 JOIN 来分解 JOIN,然后在应用程序中执行 JOIN。”
然而,一个主要缺点是它需要多次往返数据库。如果应用程序服务器和数据库相距较远,这是一个相当大的因素,会对性能产生严重影响,并可能使这种策略不可行。但如果数据库和应用程序托管在同一区域,网络开销通常可以忽略不计,并且这种方法总体上可能会更具性能优势。
智能数据库级别 JOIN 解决了冗余问题
朴素的数据库级别 JOIN 几乎从来不是从数据库中检索相关数据的最佳方法,但这是否意味着您的数据库永远不应该负责连接数据?当然不是!
近年来,数据库引擎变得非常强大,并且不断改进优化查询的方法。为了使数据库能够生成最优的查询计划,最重要的事情是它能够理解查询的*意图*。
这有两个不同的因素
- 使用 JSON 聚合等技术减少冗余
- 使用 PostgreSQL 中的
LATERAL
连接(或 MySQL 中的关联子查询)等现代数据库特性,这些特性包含查询复杂性
使用上面相同的模式示例,一个好的表示方式是
这样的查询会产生以下结果
这些数据与关于朴素数据库级别 JOIN 部分中的数据相似,除了
- 它不再包含冗余
- 帖子已经格式化为 JSON 结构
虽然此查询可能比朴素策略产生格式更好的结果,但它也变得冗长和复杂。请记住,我们总体上仍在讨论一个相对简单的场景:连接三个表,*不包含*大多数实际应用程序会处理的其他因素(例如过滤和分页)。
Prisma ORM 中 JOIN 策略的演进
当 Prisma ORM 于 2021 年首次发布时,它对其所有关系查询都实现了应用程序级别的 JOIN 策略。
当应用程序服务器和数据库彼此靠近时,这种策略非常有效,有助于跨数据库引擎的可移植性,并提高整个系统的可伸缩性(因为应用程序层 CPU 比数据库层 CPU 更容易、更便宜扩展)。
虽然应用程序级别连接的方法对大多数开发者都很有效,但有时当应用程序服务器和数据库无法彼此靠近托管时,它会引起问题,额外的往返会对整体查询性能产生负面影响。
这就是为什么我们在一年前添加了智能数据库级别 JOIN 作为替代方案,这样开发者就可以根据自己的特定用例始终选择性能最高的连接策略。
能够使用数据库级别 JOIN 一直是 Prisma ORM 最受欢迎的功能请求之一,并且自其预览版发布以来受到了社区的欢迎。一旦此功能正式发布,数据库级别 JOIN 将成为 Prisma ORM 用于其关系查询的默认 JOIN 策略。
社区反馈是我们确定工作优先级以改进 Prisma ORM 的主要驱动力之一。
结论
找出在数据库中连接来自多个表的数据的最有效方法是一个复杂的话题。在本文中,我们探讨了三种不同的方法:数据库级别的*朴素*和*智能*连接,以及*应用程序级别*连接。
朴素的数据库级别 JOIN 会在数据库服务器上产生高 CPU 开销,并由于不必要地传输冗余数据而导致网络开销。
应用程序级别 JOIN 由于其简单性和在数据库层面的廉价执行,可能更适合许多场景。使用此策略的系统通常也更容易、更便宜进行扩展。
最后,智能数据库级别 JOIN 解决了冗余问题,可以以适合应用程序开发者需求的嵌套结构返回数据,并且总体而言更有可能被数据库引擎更好地优化。
不要错过下一篇文章!
订阅 Prisma 新闻通讯