JPA 分页查询在每次后续调用中变慢

JPA pagination query becomes slower with every subsequent call

项目有 Spring 使用 JPA 启动。我们有一辆 table 车辆 有 100 万条以上的记录。 Table 有一个索引字段 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 在 < 100 毫秒内完成。哪个好

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

  3. 更令人困惑的是,在为B获取数据后,其余类型(C,D和E)在具有0数据时各需要3000-4000ms。

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

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

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

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

Chris 说对了:可能是您的应用程序不知道上次查询“B”时它离开了哪里,结果是 (pagesize 1000):

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

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

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

...等等。

所以基本上数据库执行了多次查询,每次都增加了总查询时间,因为数据库不知道上次它离开了哪里。一种解决方案是以某种方式将 last-fetched id(主键)传递给查询并从那里开始(... AND id > :id)。也许你

我编译了一个示例应用程序来测试您的发现。在我的车辆 table 中,目前有大约 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 优化器可能会忽略索引并执行完整 table 扫描,因为索引基数可能太低。

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

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

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