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种类型的记录:
- A - 0 辆车
- B - 100000 辆
- C - 0 辆
- D - 0 辆
- E - 0 辆车
问题:
其中,为 A 获取数据时,findByType 在 < 100 毫秒内完成。哪个好
虽然在获取 B 时,首先使用 LIMIT 1000 OFFSET 0 获取大约需要 200 毫秒。但是从这里开始是下坡路,随着OFFSET值的增加,时间也会增加。当 LIMIT 为 1000 且 OFFSET 为 90000 时,findByType 需要 6000-7000 毫秒。
更令人困惑的是,在为B获取数据后,其余类型(C,D和E)在具有0数据时各需要3000-4000ms。
我不确定这里发生了什么。我在某处读到,由于 OFFSET 值高,该方法花费了很多时间。但这并不能解释为什么该方法对 C、D 和 E 会花费这么多时间。
任何输入都会有所帮助。谢谢
编辑 1:分析结果(Visual VM)
- SQL 查询正常执行,几乎不需要 150-200 毫秒,即使对于高偏移值也是如此。
- 这是出乎意料的,车辆集合在每次迭代后不断向其添加车辆记录(在分析器的内存部分观察到这一点)。我希望“活动对象”计数保持在最大 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):
- 查询 A(0 个条目)花费了大约 10 毫秒。
- 查询 B(0 个条目)花费了大约 2200 毫秒。
- 查询 C(0 个条目)大约需要 10 毫秒。
- 查询 D(0 个条目)用了大约 10 毫秒。
- 查询 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() 使其释放对其中所有托管实体的引用,如果您没有对它们的应用程序引用,则允许它们被垃圾回收。
项目有 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种类型的记录:
- A - 0 辆车
- B - 100000 辆
- C - 0 辆
- D - 0 辆
- E - 0 辆车
问题:
其中,为 A 获取数据时,findByType 在 < 100 毫秒内完成。哪个好
虽然在获取 B 时,首先使用 LIMIT 1000 OFFSET 0 获取大约需要 200 毫秒。但是从这里开始是下坡路,随着OFFSET值的增加,时间也会增加。当 LIMIT 为 1000 且 OFFSET 为 90000 时,findByType 需要 6000-7000 毫秒。
更令人困惑的是,在为B获取数据后,其余类型(C,D和E)在具有0数据时各需要3000-4000ms。
我不确定这里发生了什么。我在某处读到,由于 OFFSET 值高,该方法花费了很多时间。但这并不能解释为什么该方法对 C、D 和 E 会花费这么多时间。
任何输入都会有所帮助。谢谢
编辑 1:分析结果(Visual VM)
- SQL 查询正常执行,几乎不需要 150-200 毫秒,即使对于高偏移值也是如此。
- 这是出乎意料的,车辆集合在每次迭代后不断向其添加车辆记录(在分析器的内存部分观察到这一点)。我希望“活动对象”计数保持在最大 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):
- 查询 A(0 个条目)花费了大约 10 毫秒。
- 查询 B(0 个条目)花费了大约 2200 毫秒。
- 查询 C(0 个条目)大约需要 10 毫秒。
- 查询 D(0 个条目)用了大约 10 毫秒。
- 查询 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() 使其释放对其中所有托管实体的引用,如果您没有对它们的应用程序引用,则允许它们被垃圾回收。