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 的查询

始终为 JPA @Id 使用原始对象包装器而不是原始类型?

Postgres:强制分析器使用位图扫描而不是索引扫描

Spring Data JPA 获取列表始终返回至少一个结果

spring data jpa 能不能只返回一个字段的值,而不是整个对象

触发额外查询以获取序列下一个值-spring JPA