NPlus1

N + 1 是 ORM 中用来描述加载单个对象图需要 N + 1 个 SQL 查询的情况的一般术语。当数据库通过网络远程连接时(最常见的情况),这种情况会导致非常糟糕的性能。一个好的 ORM 会提供良好的机制来缓解或避免这种情况。

默认情况下,Ebean ORM 以 10 的批大小应用批处理延迟加载,因此对于一个完全未调整的查询,您会观察到 1 + N/10 个 SQL 查询。

N 是什么?

当在 OneToMany 或 ManyToMany 关系上调用延迟加载时,N 是不同的父 ID 的数量。例如,如果您查询 100 个订单并延迟加载订单详细信息(OneToMany),则 N 是不同的订单 ID 的数量。

当延迟加载 ManyToOne 或 OneToOne 关系时,N 是该 Bean 类型不同 ID 的数量。例如,如果您查询 100 个订单并延迟加载客户,则 N 是与这些订单相关的不同客户 ID 的数量。

N 是每个“路径”

延迟加载根据遍历对象图的哪些“路径”而调用。如果您获取订单并从订单遍历到“客户”和“详细信息”,实际上是 2 个不同的路径,并且会触发 2 个不同的延迟加载查询。

例如,获取 100 个订单,然后从每个订单遍历到“客户”和“详细信息”,根据延迟加载批大小,可能会导致 3 到 201 个 SQL 查询。

1 +(客户数量)/批大小 +(订单数量)/批大小 SQL 查询。

  • 批大小 1 导致最多:1 + 100/1 + 100/1 = 最多 201 个 SQL 查询
  • 批大小 10 导致最多:1 + 100/10 + 100/10 = 最多 21 个 SQL 查询
  • 批大小 100 导致最多:1 + 100/100 + 100/100 = 最多 3 个 SQL 查询
N + 1 问题取决于遍历的路径/使用了对象图的哪些部分。

延迟加载批大小

对于 Ebean,默认延迟加载批大小为 10。您可以更改延迟加载批大小

  • 通过 ServerConfig.setLazyLoadBatchSize() 全局设置
  • 通过 Query.setLazyLoadBatchSize() 每查询设置
  • 通过 Query.fetch() 和 FetchConfig 逐个查询路径

全局默认值

如果您想将全局默认延迟加载批处理大小更改为 100,可以通过 ServerConfig.setLazyLoadBatchSize() 设置。

serverConfig.setLazyLoadBatchSize(100);

... 或通过 ebean.properties

ebean.lazyLoadBatchSize=100

逐个查询

如果您想在特定查询中更改延迟加载批处理大小,可以通过 Query.setLazyLoadBatchSize() 设置。

List<Order> orders = database.find(Order.class)
    // set lazy loading batch size for this query
    .setLazyLoadBatchSize(100)
    ...
    .findList();

逐个路径

如果您想在特定查询中更改延迟加载批处理大小,可以通过 FetchConfig 设置。

List<Order> orders = database.find(Order.class)

  // lazy fetch customer using batchSize of 10
  .fetch("customer", new FetchConfig().lazy(10))

  // eager fetch details using batchSize of 100
  .fetch("details", new FetchConfig().query(100))
  ...
  .findList();

热加载

Ebean 查询语言允许您指定热获取的“路径”。您可以选择指定要使用的批处理大小,如果不指定,批处理大小默认为 100。

List<Order> orders = database.find(Order.class)

  // specify some predicates
  .status.eq(Order.Status.NEW)
  .orderDate.after(since)

  // specify 'what to fetch'

  // eagerly fetch customer and details using batchSize of 100
  .fetch("customer")
  .fetch("details")
  .findList();

ORM 查询到 SQL 查询

ORM 查询如何分解为多个 SQL 查询

为了以下目的,Ebean 会自动将单个 ORM 查询分解为多个 SQL 查询

  • 不生成 SQL 笛卡尔积
  • 始终遵守 FirstRows/MaxRows

Ebean 始终遵守 firstRows/maxRows,并且出于性能原因,绝不会生成 SQL 笛卡尔积。这意味着 Ebean 将分析为查询定义的所有“获取路径”,并检测哪些路径包含 OneToMany 或 ManyToMany 关系。对于 Ebean,单个 SQL 查询最多可以包含一个包含 OneToMany/ManyToMany 的路径,并且 Ebean 将根据此内容自动分解 ORM 查询。

请参阅 查询联接

问答

问:全局 lazyLoadBatchSize 为 100 是否合理?

答:是的,我认为这是合理的,并且可能比 10 更好的默认值(甚至 100 应该成为 Ebean 的默认值)。较小批处理大小的优势在于对象图使用不是“对称”的情况。也就是说,在迭代“根级别对象”列表时,遍历/使用的对象图部分并不相同。但这似乎很少见,而且对于所有根级别对象,所使用的对象图部分相同的情况似乎更常见。

如果批处理大小为 100,但批处理包含的少于此值,则根本无关紧要。为了解释这一点,生成的 SQL IN 子句中的绑定值数量分为 1、5、10、20、50 和 100 个桶,其中一些绑定值被重复以填满桶。为什么要这样做?我们不希望有太多不同的 SQL 语句,因为数据库本身会解析和缓存每个 SQL 语句的执行计划,因此限制桶可以让我们在数据库的 SQL 执行计划缓存中获得更好的命中率。

问:这与 AutoTune 有什么关系?

答: Ebean 的 AutoTune 功能会分析对象图的哪些部分被使用,并利用此信息自动调整 fetch() 和 select() 子句的查询,从而减少延迟加载并用急加载替换它。遍历 ManyToOne 和 OneToOne 的路径可以组合在一起,从而减少执行的 SQL 查询总数,因此这比使用大批量大小的批量延迟加载更好。

问:如何识别 N + 1 问题?

答:在 Ebean 术语中,1 与origin query相关,N 与secondary queries相关。在日志记录中,你可以为org.avaje.ebean.SUM打开TRACE级别日志记录,并且日志条目中包含一个名为origin的属性。origin 是一个哈希,用于将辅助查询链接回原始查询。对该 origin 键进行低技术 grep 将返回 origin 查询和所有相关的延迟加载查询。

很快将提供仪表板/监控服务,它将能够识别导致大量延迟加载的 origin 查询。

AutoTune 分析提供了另一种选择,尽管它面向提供修复过度延迟加载的调整查询。

问:JPA FetchType.EAGER 怎么样?

答:作为部署注释,FetchType 提供了一个提示,适用于单个用例。问题在于,bean 及其属性/关系需要支持许多用例 - 通过部署注释进行优化可以针对 1 个用例进行优化,但可能会损害许多其他用例。

也就是说,ORM 应该提供一种机制/查询语言,使开发人员能够针对每个用例优化对象图构造函数,而固定的部署注释无法做到这一点。

问:JPA Fetch 组怎么样?

答:Fetch 组对于 JPA 用户来说是一件好事,它提供了一种控制在对象图中获取哪个部分的功能,类似于 Ebean 的fetch()select()

我对 JPA Fetch 组的个人失望是

  • Fetch 组看起来很冗长且难以使用
  • Fetch 组应该是 JPQL 查询语言的一部分
  • Fetch 组缺少对以下内容的控制:急加载/延迟加载、批量大小/只读、L2/L3 使用
  • Fetch 组是一个提示?你说什么?

问:JPQL 怎么样?

答:JPQL 不是一个用于优化对象图构造的好查询语言,Ebean 因此没有采用 JPQL。也就是说,在使用 Ebean 构建对象图时,我们希望为开发人员提供以下功能

  • 控制填充对象图的哪一部分(哪些路径)
  • 对急切/延迟加载进行每路径控制
  • 对加载批大小进行每路径控制
  • 对只读和 L2/L3 缓存使用进行每路径控制

Ebean 的 ORM 查询语言旨在提供一种功能强大且简单的方法来控制和优化对象图的构建。当 ORM 查询语言不太合适(聚合查询、报告、递归查询等)时,Ebean 可与 SQL 良好地集成。ORM 查询语言可能涵盖 90% 以上的 OLTP 查询,但我们不想扩展它并使语言复杂化,而是提倡使用良好的机制来与原始 SQL 集成。