February 21, 2024

Prisma ORM 现在允许您选择最佳的 Join 策略(预览版)

从 SQL 数据库中的多个表获取相关数据可能会非常耗时。Prisma ORM 现在允许您在 数据库层面应用层面 Join 之间进行选择,以便您可以为您的关联查询选择性能最佳的方法。

目录

Prisma ORM 新功能:选择最佳的 Join 策略 🎉

支持数据库层面的 Join 是 Prisma ORM 中呼声最高的功能之一,我们很高兴地宣布它现在作为另一种查询策略可用了!

对于任何带有 include (或 select) 的关联查询,现在在顶层有一个新选项,叫做 relationLoadStrategy。此选项接受两个可能值中的一个

  • join (默认):使用数据库层面的 Join 策略在数据库中合并数据。
  • query:通过向单个表发送多个查询,并在应用层合并数据来使用应用层面的 Join 策略。

要启用新的 relationLoadStrategy,您首先需要将预览功能标志添加到 Prisma Client 的 generator 块中

注意relationLoadStrategy 仅适用于 PostgreSQL 和 MySQL 数据库。

完成后,您需要重新运行 prisma generate 使此更改生效,并在您的查询中选择关联加载策略。

这是一个使用新 join 策略的示例

请注意,由于 "join" 是默认值,因此在上面的代码片段中技术上也可以省略 relationLoadStrategy 选项。我们在此处显示它仅用于说明目的。

join vs query — 何时使用哪个?

现在有了这两种查询策略,您可能会想:何时使用哪个?

由于 Prisma ORM 在 PostgreSQL 上使用了横向的、聚合的 JOIN 以及在 MySQL 上使用了相关子查询,join 策略在大多数情况下可能会更高效(稍后部分将对此有更多详细说明)。数据库引擎非常强大,并且擅长优化查询计划。这种新的关联加载策略正是基于这一点。

但是,在某些情况下,您可能仍希望使用 query 策略来对每个表执行一个查询,并在应用层面合并数据。根据数据集和 schema 中配置的索引,发送多个查询可能会获得更好的性能。对您的查询进行性能分析和基准测试将是识别这些情况的关键。

另一个需要考虑的因素是复杂 Join 查询带来的数据库负载。如果由于某种原因,数据库服务器上的资源稀缺,您可能希望将带有过滤和分页的复杂 Join 查询所需的大量计算转移到您的应用服务器,这可能更容易扩展。

总结

  • 新的 join 策略在大多数场景下会更高效。
  • 在某些边缘情况下,根据数据集和查询的特征,query 可能会更高效。我们建议您对数据库查询进行性能分析以识别这些场景。
  • 如果您想节省数据库服务器上的资源,并在可能更容易扩展的应用服务器中执行数据合并和转换的繁重工作,请使用 query

理解 SQL 数据库中的关联

现在我们了解了 Prisma ORM 的 JOIN 策略,接下来回顾一下关联查询在 SQL 数据库中通常是如何工作的。

关联数据的扁平 vs 嵌套结构

SQL 数据库以 扁平化 (即 范式化)的方式存储数据。实体之间的关联通过 外键 来表示,外键指定了跨表的引用。

另一方面,应用开发者通常习惯于处理 嵌套 数据,即可以任意深度嵌套其他对象的对象。

这是一个巨大的差异,不仅体现在数据在磁盘和内存中 物理 布局的方式上,也体现在关于数据的 心智模型 和推理上。

关联数据需要为应用开发者进行“合并”

由于关联数据在数据库中物理上是分开存储的,因此需要在某个地方进行 合并,以便成为应用开发者熟悉的嵌套结构。这种合并也称为“join”。

这种 Join 可以在两个地方发生

  • 数据库层面:向数据库发送单个 SQL 查询。该查询使用 JOIN 关键字或相关子查询来让数据库执行跨多个表的 Join,并返回嵌套结构。
  • 应用层面:向数据库发送多个查询。每个查询只访问一个表,然后查询结果在应用层内存中合并。在 v5.9.0 之前,这是 Prisma Client 唯一支持的查询策略。

哪种方法更可取取决于所使用的数据库、数据集的大小和特征以及查询的复杂性。请继续阅读以了解何时推荐使用哪种策略。

内部原理是什么?

Prisma ORM 使用 PostgreSQL 中的 LATERAL Join 和数据库层面的 JSON 聚合(例如通过 json_agg)以及 MySQL 中的相关子查询来实现新的 join 关联加载策略。

在接下来的部分中,我们将探讨为什么 PostgreSQL 上的 LATERAL Join 和数据库层面的 JSON 聚合方法比普通的传统 JOIN 更高效。

使用 JSON 聚合防止查询结果冗余

使用数据库层面的 JOIN 时,构建 SQL 查询有多种选项。让我们考虑上面 Prisma schema 的 SQL 表定义

要检索所有用户及其帖子,您可以使用一个简单的 LEFT JOIN 查询

使用一些示例数据,结果可能如下所示

请注意在这种情况下 user_name 列中的冗余。Join 的表越多,这种冗余就越严重。例如,假设有另一个 Comment 表,其中每条评论都有一个指向 Post 表中记录的 postId 外键。

这是一个表示该关联的 SQL 查询

现在,假设第一篇帖子有多条评论

在这种情况下,结果集的大小随着 Join 的表数量呈指数增长。由于这些数据通过网络从数据库传输到应用服务器,这可能会变得非常昂贵。

Prisma 在数据库层面实现的使用 JSON 聚合的 join 策略解决了这个问题。

这是一个 PostgreSQL 示例,使用 json_aggjson_build_object 解决冗余问题,并以 JSON 格式返回每个用户的帖子

此次的结果集不包含冗余数据。此外,数据结构已经方便地具备了 Prisma Client 返回的形状,这省去了在查询引擎中转换结果的额外工作。

横向 Join(Lateral JOIN)提高带分页和过滤查询的效率

关联查询(像大多数其他查询一样)几乎从不从表中获取 全部 数据,而是带有额外的结果集约束,例如过滤和分页。特别是分页在使用传统 JOIN 时会变得非常复杂,让我们来看另一个例子。

考虑这个 Prisma Client 查询,它获取 10 个用户以及每个用户的 5 篇帖子

在编写原始 SQL 时,您可能会想在子查询中使用 LIMIT 子句,例如

然而,这不会起作用,因为内部的 SELECT 实际上并没有返回每位用户 五篇帖子 —— 而是返回了 总共 两篇帖子,这显然不是期望的结果。

使用传统的 JOIN,可以通过使用 row_number() 函数为结果集中的记录分配递增的整数来解决此问题,从而手动执行分页计算。

然而,这种方法很快变得非常复杂,因此不适合构建带分页的关联查询。

维护、扩展和调试这类 SQL 查询非常困难,并且可能消耗数小时的开发时间。

值得庆幸的是,较新的数据库版本通过一种新型查询解决了这个问题:横向 JOIN(lateral JOIN)

通过使用 LATERAL 关键字,上面的查询可以简化

这不仅使查询更具可读性,而且数据库引擎也可能更有能力优化查询,因为它更能理解查询的 意图

结论

让我们回顾一下使用 Prisma 从关联查询中 Join 数据的不同选项。

过去,Prisma 仅支持应用层面的 Join 策略,该策略向数据库发送多个查询,并在查询引擎内部完成所有合并和转换为预期 JavaScript 对象结构的工作

使用普通的传统 JOIN 时,数据的合并工作将委托给数据库。然而,如上所述,存在数据冗余(结果集随着关联查询中的表数量呈指数增长)以及包含过滤和分页的查询复杂性问题

为了解决这些问题,Prisma ORM 在数据库层面实现了现代的横向 JOIN,并辅以 JSON 聚合。这样,解析查询并将数据转换为预期的 JavaScript 对象结构所需的所有繁重工作都在数据库层面完成。

试用并分享您的反馈

我们非常希望您试用新的关联查询加载策略。请告诉我们您的想法并与我们分享您的反馈

不要错过下一篇帖子!

订阅 Prisma 新闻通讯