Spring JPA 查询始终使用序列扫描而不是索引扫描
Posted
技术标签:
【中文标题】Spring JPA 查询始终使用序列扫描而不是索引扫描【英文标题】:Spring JPA query always uses Sequence Scan instead of an Index Scan 【发布时间】:2018-05-05 15:09:49 【问题描述】:我有一个简单的查询
@Query(value = "select * from some_table where consumer_id=:consumerId and store_id=:storeId and cancelled_at is null", nativeQuery = true)
fun checkIfNewConsumer(consumerId: BigInteger, storeId: BigInteger): List<SomeClass?>
当我直接对超过 3000 万行的表运行带有解释的查询时
Index Scan using select_index on some_table (cost=0.56..8.59 rows=1 width=86) (actual time=0.015..0.015 rows=0 loops=1)
Index Cond: ((consumer_id = 1234) AND (store_id = 4) AND (cancelled_at IS NULL))
Planning time: 0.130 ms
Execution time: 0.042 ms
当我使用 Spring Boot 通过请求运行相同的查询时:
"Plan"=>"Total Cost"=>1317517.92, "Relation Name"=>"some_table", "Parallel Aware"=>"?", "Filter"=>"?", "Alias"=>"some_table", "Node Type"=>"Seq Scan", "Plan Width"=>86, "Startup Cost"=>0.0, "Plan Rows"=>912
Execution time: 9613 ms
上面的春季启动计划来自新的遗物。 如您所见,它默认为每个查询使用 Seq scan,而不是 Index scan。我已经真空分析假设它是数据库(没有骰子),我尝试了查询的变体,没有骰子。它在plsql中总是看起来很完美,通过spring borks。
我们将不胜感激任何建议。
编辑 2:潜在解决方案
我们发现,通过禁用准备好的语句,将 ?preferQueryMode=simple
添加到您的连接 URL:jdbc:postgresql://localhost:5432/postgres?preferQueryMode=simple
得到了使用索引扫描的查询。
我们需要了解如何?为什么?为什么是现在?
编辑 1:技术栈
弹簧靴2.0M5 科特林 PostgreSQL 9.6.2编辑:解决方案@Vlad Mihalcea
请不要使用 preferQueryMode=simple,除非您完全确定它的含义。显然,https://gist.github.com/vlsi/df08cbef370b2e86a5c1 中描述了您的问题。我猜你在数据库中有 BigInt,在 Kotlin 代码中有 BigInteger。你可以在 Kotlin 中使用 Long 吗?
–弗拉基米尔·希特尼科夫
【问题讨论】:
请不要使用preferQueryMode=simple
,除非您完全确定它的含义。显然,gist.github.com/vlsi/df08cbef370b2e86a5c1 中描述了您的问题。我猜你在数据库中有bigint
,在Kotlin 代码中有BigInteger
。你可以在 Kotlin 中使用 Long
吗?
您能否将其发布为答案以便我接受?这确实是解决方案
【参考方案1】:
由于 PostgreSQL 不需要任何执行计划缓存,并且 PreparedStatement(s)
实际上是模拟的,直到达到给定的执行阈值(例如 5),我认为这是您在这里面临的索引选择性问题。
如果此查询仅返回少量记录,则数据库将使用索引。
如果此查询将返回大量记录,则数据库将不使用索引,因为随机访问页面读取的成本将高于顺序扫描的成本。
因此,您可能在这里使用了不同的绑定参数值集。
-
您在 pgsql 控制台中给出的那些是高度选择性的,因此您会得到索引扫描。
您在运行时发送的可能不同,因此您会获得顺序扫描。
此外,在 pgsql 上,解释计划不会考虑将所有记录发送到 JDBC 驱动程序的网络开销。但是,这是对您的问题的补充,而不是实际的根本原因。
现在,要真正确定实际的执行计划,请尝试在 PostgreSQL 中启用 auto_explain
模式。
或者,您可以编写一个运行查询的测试方法,如下所示:
List<Object[]> executionPlanLines = doInJPA(entityManager ->
try(Stream<Object[]> postStream = entityManager
.createNativeQuery(
"EXPLAIN ANALYZE " +
"select * from some_table where consumer_id=:consumerId and store_id=:storeId and cancelled_at is null ")
.setParameter("consumerId", consumerId)
.setParameter("storeId", storeId)
.unwrap(Query.class)
.stream()
)
return postStream.collect( Collectors.toList() );
);
LOGGER.info( "Execution plan: ",
executionPlanLines
.stream()
.map( line -> (String) line[0] )
.collect( Collectors.joining( "\n" ) )
);
这样,您将看到在生产中运行的实际执行计划。
【讨论】:
首先,感谢您的详细解释。我将启用 auto_explain。查询参数相同,我从数据库日志和应用程序日志中复制它们进行比较。相同的查询相同的参数。我还尝试了带引号和不带引号的参数(在 CLI 上两次都使用索引扫描) 确实很奇怪。 我们可以在没有 stream() 的情况下使用 JPA 2.0 做到这一点吗? 使用 Hibernate,您可以从 3.x 开始滚动。和流一样。【参考方案2】:请不要使用preferQueryMode=simple
,除非您完全确定它的含义(例如,它可能有助于处理逻辑复制流)。
显然您的问题在https://gist.github.com/vlsi/df08cbef370b2e86a5c1 中有所描述。我猜你在数据库中有bigint
,在Kotlin 代码中有BigInteger
。你可以在 Kotlin 中使用 Long
吗?
以防万一:PostgreSQL 中的bigint
表示int8
,因此应在应用程序中使用Long
。
另一种选择是添加如下显式转换:consumer_id=cast(:consumerId as bigint) and store_id=cast(:storeId as bigint)
。
问题与“字符列与数值比较”相同,但是这里的区别更微妙(int8 vs numeric)
【讨论】:
以上是关于Spring JPA 查询始终使用序列扫描而不是索引扫描的主要内容,如果未能解决你的问题,请参考以下文章
使用 Spring JPA 从序列中获取 nextval 的查询
Spring Data JPA 获取列表始终返回至少一个结果