【发布时间】:2021-08-27 14:10:57
【问题描述】:
我们使用 SQL Server 数据库。
当尝试使用 27GB 内存对具有 100M 条记录的表运行查询时,Hibernate 会将查询(无论是 findById 还是 findByFieldName)转换为如下内容:
select sbtinvento0_.cust_nbr as cust_nbr1_10_0_, sbtinvento0_.ean_upc as ean_upc2_10_0_, sbtinvento0_.rep_bal_qty as rep_bal_3_10_0_ from odh_inv.sbt_inven sbtinvento0_ where sbtinvento0_.cust_nbr=? and sbtinvento0_.ean_upc=?
现在发生的是这个查询持续了大约 7 秒。数据库中的两个字段都是字符串类型。
我们能够在 DBeaver 中重现此行为:
SELECT CUST_NBR, EAN_UPC, REP_BAL_QTY
FROM ODH_INV.SBT_INVEN
WHERE CUST_NBR = 0000100974 AND EAN_UPC = '0042823091698';
此查询也将持续 7 秒。但是,如果按照以下方式进行查询,只需要 0.7 秒:
SELECT CUST_NBR, EAN_UPC, REP_BAL_QTY
FROM CommonSQLDevPrj.ODH_INV.SBT_INVEN
WHERE CUST_NBR = '0000100974' AND EAN_UPC = '0042823091698';
这种行为在多个应用程序/环境/数据库、具有数百万行的表上、在使用带有字符串参数的 findById 或 findByFieldName 时由 Hibernate 自动创建的查询上重复。
我尝试在谷歌上搜索此问题,但找不到任何有用的信息。是否可以在 application.yml 文件中设置任何属性以强制在字符串参数上使用撇号?
我尝试了以下方法:
@Query("SELECT si FROM SbtInventory si WHERE sbtInventoryId.customerNumber = ':customerNumber' and sbtInventoryId.eanUpc = ':eanUpc'")
Optional<SbtInventory> findByCustomerNumberAndEanUpc(String customerNumber, String eanUpc);
但是查询按原样运行,它不会用函数参数中的值替换查询参数。我尝试使用 ?1 和 ?2,或者转义撇号,但它们都不起作用。
我还尝试了一些使用实体管理器和会话的解决方法。
使用 Entity Manager 的第一个解决方法确实可以在 1 秒内完成。问题是我必须在查询中手动添加参数。在这种情况下,这不是问题,因为这是一项工作,我们保证这里不会有任何 SQL 注入,但由于这个原因,不建议使用此方法,我们将无法使用它在 API 上。
private final EntityManager em;
public Optional<SbtInventory> get(String customerNumber, String eanUpc) {
@SuppressWarnings("unchecked")
List<SbtInventory> sbtInventory = em
.createQuery("SELECT si FROM SbtInventory si WHERE sbtInventoryId.customerNumber = '" + customerNumber
+ "' and sbtInventoryId.eanUpc = '" + eanUpc + "'")
.getResultList();
if (sbtInventory.size() == 0) {
return Optional.empty();
} else {
return Optional.of(sbtInventory.get(0));
}
}
推荐的第二种方法,使用 Session,也可以,但查询再次需要 7 秒才能运行:
private final EntityManager em;
@Transactional
public Optional<SbtInventory> get(String customerNumber, String eanUpc) {
Session session = null;
if (em == null || (session = em.unwrap(Session.class)) == null) {
throw new NullPointerException();
}
@SuppressWarnings("unchecked")
List<SbtInventory> sbtInventory = session.createQuery(
"SELECT si FROM SbtInventory si WHERE sbtInventoryId.customerNumber = :customerNumber and sbtInventoryId.eanUpc = :eanUpc")
.setParameter("customerNumber", customerNumber, StandardBasicTypes.STRING)
.setParameter("eanUpc", eanUpc, StandardBasicTypes.STRING).getResultList();
if (sbtInventory.size() == 0) {
return Optional.empty();
} else {
return Optional.of(sbtInventory.get(0));
}
}
但所有这些都是不受欢迎的,因为它为我们的查询添加了许多额外的代码和额外的处理,所以我仍然希望我们可以设置一些休眠属性,以便它自己添加撇号。
编辑: 我尝试了 Jens 的演员建议:
SELECT CUST_NBR, EAN_UPC, REP_BAL_QTY
FROM ODH_INV.SBT_INVEN
WHERE CUST_NBR = CAST(0000100974, varchar) AND EAN_UPC = '0042823091698';
此查询运行速度很快,但没有返回任何结果。我认为这可能是因为删除了前导零,所以我尝试了:
SELECT CUST_NBR, EAN_UPC, REP_BAL_QTY
FROM ODH_INV.SBT_INVEN
WHERE CUST_NBR = RIGHT(CONCAT('0000000000', CAST(0000100974 AS varchar)), 10) AND EAN_UPC = '0042823091698';
这终于可以工作了,但它意味着手动编写查询,这让我可以在存储库中使用 @Query 而不是使用实体管理器,这是一种升级(如果它实际上可以在周一工作,则无法测试它) Hibernate ),但仍然意味着像这样编写每个敏感查询,而不是简单地使用由函数名形成的查询:(
编辑 2: 我将在这里回答一些小工具的问题
您的 SbtInventory 实体中 customerNumber 的休眠映射是否有任何自定义?能否请您发布映射?
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
@Builder
@Table(name = "SBT_INVEN", schema = "ODH_INV")
public class SbtInventory {
@EmbeddedId
private SbtInventoryIdentity sbtInventoryId;
@Column(name = "REP_BAL_QTY")
private Integer reportedBalanceQuantity;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = false)
@Embeddable
public class SbtInventoryIdentity implements Serializable {
/**
*
*/
private static final long serialVersionUID = -892835625356278964L;
@Column(name = "CUST_NBR")
private String customerNumber;
@Column(name = "EAN_UPC")
private String eanUpc;
}
还有你用什么方言来休眠?
数据库连接的完整配置属性:
datasource:
url: jdbc:sqlserver://db-url:db-port;databaseName=db-name
username: user
password: pass
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
hikari:
data-source-properties:
cachePrepStmts: true
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
useServerPrepStmts: false
maximum-pool-size: 50
jpa:
database-platform: org.hibernate.dialect.SQLServer2008Dialect
database: SQL_SERVER
show-sql: true
hibernate:
ddl-auto: none
properties:
hibernate.dialect: org.hibernate.dialect.SQLServer2008Dialect
hibernate.jdbc.batch_size: 20
hibernate.cache.use_second_level_cache: false
hibernate.enable_lazy_load_no_trans: true
在 db 中使用 char 而不是 varchar 的任何特殊原因?
这是一个我没有确切答案的问题。这就是数据库的设计方式。客户编号是另一个表的外键,其中主键被创建为 Char。这些是一些旧表,为了保持该标准,它们继续使用 char 作为遗留字段。
编辑 3:
@Query(value = "SELECT REP_BAL_QTY\r\n" + "FROM ODH_INV.SBT_INVEN\r\n"
+ "WHERE CUST_NBR = RIGHT(CONCAT('0000000000', CAST(:customerNumber AS varchar)), 10) "
+ "AND EAN_UPC = RIGHT(CONCAT('0000000000000', CAST(:eanUpc AS varchar)), 13)", nativeQuery = true)
Optional<Integer> findReportedBalanceQuantityByCustomerNumberAndEanUpc(String customerNumber, String eanUpc);
所以这在存储库中工作正常,但在这种情况下,它是我要公开的 API,我只需要结果中的 REP_BAL_QTY 列,所以我必须为页面的每一行运行此查询我'我回来了。在我将其映射到实体之前:
@MapsId
@OneToOne
@JoinColumns({
@JoinColumn(name = "EAN_UPC", referencedColumnName = "EAN_UPC"),
@JoinColumn(name = "CUST_NBR", referencedColumnName = "CUST_NBR")
})
@NotFound(action = NotFoundAction.IGNORE)
private SbtInventory sbtInventory;
我认为这是在加载实体时调用的,并且过程有点快(除了显而易见的,避免样板代码)
使用实体中的join,带10行页面的过程持续:
SbtInventoryController.getData 耗时 84113 毫秒
这是连接的执行查询之一:
select sbtinvento0_.CUST_NBR as CUST_NBR1_19_0_, sbtinvento0_.EAN_UPC as EAN_UPC2_19_0_, sbtinvento0_.REP_BAL_QTY as REP_BAL_3_19_0_ from ODH_INV.SBT_INVEN sbtinvento0_ where sbtinvento0_.CUST_NBR=? and sbtinvento0_.EAN_UPC=?
使用查询,过程持续:
SbtInventoryController.getData 耗时 14063 毫秒
所以这是一个巨大的性能飞跃。缺点是通过使用查询,我必须放弃对包含 REP_BAL_QTY 的列进行排序:(
【问题讨论】:
-
SbtInventory 实体中 customerNumber 的 java 类型是什么?
-
CUST_NBR的类型是什么?如果是VARCHAR或类似的,那么您在开头提到的两个语句是非常不同的。您可以尝试强制转换绑定参数w3schools.com/sql/func_sqlserver_cast.asp -
@gadget 对于 customerNumber 和 eanUpc 的类型都是 String
-
@JensSchauder CUST_NBR 是 char(10) 而 EAN_UPC 是 char(13)
-
用@JensSchauder 的建议更新了问题
标签: java sql-server spring hibernate spring-data-jpa