【问题标题】:JPA pagination query becomes slower with every subsequent call每次后续调用时,JPA 分页查询都会变慢
【发布时间】:2022-01-25 14:46:16
【问题描述】:

项目具有带有 JPA 的 Spring Boot。我们有一个表 vehicle 有 100 万多条记录。表有一个索引字段type

我们有一个用例,我们想按类型获取所有记录。对于每种类型,我们获取所有车辆记录,然后是下一个类型,然后是下一个,依此类推。

由于有 1m+ 条记录,我们以批量大小为 1000 的方式获取每种类型的记录。我们还应用了带有类型列的过滤器。

VehicleRepository.java

Page<VehicleRecord> findByType(String type, Pageable pageable);

VehicleService.java

for (String type: vehicleTypes) {

  Pageable pageable = PageRequest.of(0, 1000, Sort.by("updated_at").ascending());
  Page<VehicleRecord> vehicles = null;

  do {
    vehicles = vehicleRepository.findByType(type, pageable);
    // do something with vehicles
    pageable = pageable.next();
  } while (vehicles.hasNext());

}

为了便于理解,假设有5种记录:

  1. A - 0 辆车
  2. B - 100000 辆
  3. C - 0 辆车
  4. D - 0 辆车
  5. E - 0 辆车

问题:

  1. 在这种情况下,当为 A 获取数据时,findByType 在

  2. 当为 B 获取时,首先使用 LIMIT 1000 OFFSET 0 获取大约需要 200 毫秒。但从这里开始走下坡路,随着OFFSET值的增加,时间也随之增加。到 LIMIT 为 1000 且 OFFSET 为 90000 时,findByType 需要 6000-7000 毫秒。

  3. 更令人困惑的是,在为 B 获取数据后,其余类型(C、D 和 E)在它们有 0 个数据时每个需要 3000-4000 毫秒。

我不确定这里发生了什么。我在某处读到,由于高 OFFSET 值,该方法花费了这么多时间。但这并不能解释为什么该方法对 C、D 和 E 需要这么多时间。

任何输入都会有所帮助。谢谢

编辑 1:分析结果(Visual VM)

  1. SQL 查询正在正常执行,它们几乎不需要 150-200 毫秒,即使对于高偏移值也是如此。
  2. 这是出乎意料的,车辆集合在每次迭代后不断向其添加车辆记录(在分析器的内存部分中观察到这一点)。我希望“活动对象”计数保持在最大 1000,因为这就是我们的限制大小。但在每次迭代之后,它会不断向其中添加 1000 条记录。即使在从分析器执行手动 GC 之后,它也不会释放内存,直到 for 循环的所有迭代都完成。

【问题讨论】:

  • 可能实体管理器没有清除?也许有人试图在缓存中查找东西。这是在单笔交易中完成的吗?
  • 向后循环时会发生什么? (e -> a)
  • 2 美分:比较偏移量和键集分页怎么样?也许这也可以解释一些放缓? use-the-index-luke.com/no-offset
  • @Chris 我尝试分析,查询运行正常,不需要太多时间,总是
  • 我不确定我是否理解您对结果的看法 - 听起来您的查询本身没有问题。听起来不错!我不知道活动对象指的是什么,或者您的设置 - 这是您读入 JVM 的对象数量,并且在您查询后被保留?分页很棒,但您可能对所有页面使用相同的 entityManager 实例 - JPA 要求它们由持久性单元“管理”,直到 EM 完成。您在 VehicleService 中显示的循环并不能清楚地说明您在做什么。可能只需要一个 em.clear()

标签: java spring-boot jpa spring-data-jpa garbage-collection


【解决方案1】:

Chris 说对了:可能是您的应用程序不知道上次查询“B”时它离开的位置,发生的情况是(页面大小 1000):

您请求页面 0: 查找匹配的条目并将它们添加到结果集中。一旦结果集的大小为 1000,就返回它。

您请求页面 1: 查找 (!) 并跳过前 1000 个匹配条目。取匹配的条目 1001 到 2000,将它们添加到结果集中并忽略它。

您请求第 2 页: 查找 (!) 并跳过前 2000 个匹配条目。取匹配的条目 2001 到 3000,将它们添加到结果集中并忽略它。

...等等。

所以基本上数据库会多次执行查询,每次都会增加总查询时间,因为数据库不知道上次离开的位置。一种解决方案是以某种方式将最后获取的 id(主键)传递给查询并从那里开始(... AND id &gt; :id)。也许你

我编译了一个示例应用程序来测试您的发现。在我的车辆表中,目前有 ~723k 条目。数据库和应用程序在我的本地机器上运行(页面大小 1000):

  1. 查询 A(0 个条目)大约需要 10 毫秒。
  2. 查询 B(0 个条目)大约需要 2200 毫秒。
  3. 查询 C(0 个条目)大约需要 10 毫秒。
  4. 查询 D(0 个条目)大约需要 10 毫秒。
  5. 查询 E(0 个条目)大约需要 10 毫秒。

所以,我无法重现您的问题。也许您可以将代码缩减为尽可能简单并与我们分享(或自己找到瓶颈)。

我把我的上传到my Github repository.

结果是:

A: 185ms
B: 2139ms
B: 2007ms
B: 1863ms
B: 1930ms
C: 2ms
D: 3ms
E: 2ms
A: 1ms
B: 2020ms
B: 2044ms
B: 2006ms
B: 2053ms
B: .. same average values all over

要提一件事,如果您的数据库中有很多记录,但只有少量不同类型的记录,那么索引将无济于事。某些 SQL 优化器可能会忽略索引并执行全表扫描,因为索引基数可能太低。

【讨论】:

  • 我尝试使用 id > :id 而不是偏移量,但这会产生同样有问题的结果。代码已经是我们所做工作的简化版本。谢谢
  • 您应该查看我的示例并将其与您的示例进行比较。我的示例中的查询在每次后续调用时都不会变慢。尝试将您的示例上传到 github(或其他地方),以便我查看。
【解决方案2】:

从 cmets 看来,问题似乎不在于分页查询本身,而在于它的使用方式和数据量会影响 JVM。提供的代码 sn-p 建议您在同一个 VehicleService 方法中多次调用 vehicleRepository.findByType(type, pageable);,这意味着它们都将在同一个 EntityManager/事务上下文中。 JPA 要求 EntityManager 上下文缓存通过它们读取的每个实体,以便它们可以监视和序列化对数据库所做的任何更改。如果您正在阅读大批量的实体,这会增加 - EntityManager 旨在代表工作单元,而不是像那样长期存在。

解决方案是将每个“批次”分解为自己的事务上下文,并调用每种车辆类型。

或者,您可以获取 EntityManager 实例的句柄。处理完您的实体后,调用 EntityManager.clear() 让它释放对其中所有托管实体的引用,如果您没有应用程序对它们的引用,则允许它们被垃圾回收。

【讨论】:

  • 是的,这似乎是问题所在。我正在使用 JPA,所以从来没有想过 EntityManager 在引擎盖下。 EntityManager.clear() 修复它。
猜你喜欢
  • 2011-10-11
  • 2015-05-18
  • 1970-01-01
  • 2023-03-24
  • 1970-01-01
  • 2019-05-19
  • 2022-06-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多