单次执行后关闭PreparedStatement - 这是一个设计缺陷吗?

时间:2015-10-11 13:39:33

标签: java jdbc eda

我已经调查了很多地方,听过很多可疑的声明,PreparedStatement应该比Statement更受欢迎,即使只是为了表现的好处;一直到声称PreparedStatement s应该专门用于批量语句而不是其他任何内容。

然而,我所遵循的(主要是在线)讨论似乎存在盲点。让我提出一个具体的方案。

我们有一个带有数据库连接池的EDA设计应用程序。事件来了,其中一些需要持久性,一些则不需要。一些是人工生成的(例如,每隔X分钟更新/重置一些东西)。 有些事件会按顺序处理,但其他类型的事件(也需要持久性)可以(并且将会)同时处理。

除了那些人工生成的事件之外,没有关于需要持久性的事件如何到来的结构。

此应用程序是在很久以前设计的(大约2005年),并支持多个DBMS。典型的事件处理程序(需要持久性):

  • 从池中获取连接
  • 准备sql语句
  • 执行准备好的陈述
  • 处理结果集(如果适用),将其关闭
  • 关闭准备好的陈述
  • 如有必要,准备一份不同的陈述并以同样的方式处理
  • 返回与池的连接

如果事件需要批处理,则语句将准备一次,并使用addBatch / executeBatch方法。这是一个明显的性能优势,这些情况与此问题无关。

最近,我收到一个意见,即准备(解析)一个语句,执行一次并关闭的整个想法基本上是对PreparedStatement的滥用,无论是否有效,都提供零性能优势使用服务器或客户端预处理语句,典型的DBMS(Oracle,DB2,MSSQL,MySQL,Derby等)甚至不会将这样的语句提升为预处理语句缓存(或者至少,它们的默认JDBC驱动程序/数据源不会)

此外,我不得不在MySQL的开发环境中测试某些场景,似乎Connector / J使用分析器同意这个想法。对于所有非批处理的预准备语句,调用close()打印:

PreparedStatement created, but used 1 or fewer times. It is more efficient to prepare statements once, and re-use them many times

由于前面概述的应用程序设计选择,拥有一个PreparedStatement实例缓存,可以保存连接池中每个连接的任何事件使用的每个SQL语句,这听起来都是不好的选择。

有人可以进一步详细说明吗? 是逻辑"准备 - 执行(一次) - 关闭"有缺陷的并且基本上气馁?

P.S。在useUsageAdvisor=true上调用cachePrepStmts=true时,明确指定Connector / J的useServerPrepStmts=trueuseServerPrepStmts=false并使用close()PreparedStatement仍然会产生有关效率的警告每个非批处理SQL语句的实例。

5 个答案:

答案 0 :(得分:3)

  

逻辑准备 - 执行[一次] - 接近有缺陷且基本上不鼓励吗?

我不认为这是一个问题,本身。给定的SQL语句需要准备好#34;在某些时候,无论是明确地(使用PreparedStatement)还是"在飞行中" (附声明)。如果我们使用PreparedStatement而不是Statement来执行只会执行一次的事情,可能会产生更多的开销,但是所涉及的开销不太可能很大,特别是如果你引用的语句是真的:

  

典型的DBMS(Oracle,DB2,MSSQL,MySQL,Derby等)甚至不会将这样的语句提升为准备好的语句缓存(或者至少,它们的默认JDBC驱动程序/数据源不会)。

不鼓励的是这样的模式:

for (int thing : thingList) {
    PreparedStatement ps = conn.prepareStatement(" {some constant SQL statement} ");
    ps.setInt(1, thing);
    ps.executeUpdate();
    ps.close();
}

因为PreparedStatement只使用一次,并且一遍又一遍地准备相同的SQL语句。 (尽管如果SQL语句及其执行计划确实被缓存,即使这可能也不是什么大问题。)更好的方法是

PreparedStatement ps = conn.prepareStatement(" {some constant SQL statement} ");
for (int thing : thingList) {
    ps.setInt(1, thing);
    ps.executeUpdate();
}
ps.close();

......甚至更好,使用"尝试使用资源" ...

try (PreparedStatement ps = conn.prepareStatement(" {some constant SQL statement} ")) {
    for (int thing : thingList) {
        ps.setInt(1, thing);
        ps.executeUpdate();
    }
}

请注意,即使不使用批处理也是如此。 SQL语句仍然只准备一次并多次使用。

答案 1 :(得分:2)

正如其他人已经说过的那样,最昂贵的部分是解析数据库中的语句。如果语句已在共享池中解析,则某些数据库系统(这几乎与数据库相关 - 我将主要针对 Oracle )可能会获利。 (在Oracle术语中,这称为软解析,比硬解析 - 一个新语句的解析)便宜。即使您只使用预准备语句一次,也可以从软解析中获利。

因此,重要的任务是为数据库提供重用语句的机会。典型的计数器示例是基于Hibernate中的集合处理IN列表。你以

之类的陈述结束
 .. FROM T WHERE X in (?,?,?,  … length based on the size of the collection,?,? ,?,?)

如果集合的大小不同,则无法重复使用此语句。

了解正在运行的应用程序生成的SQL查询范围的一个很好的起点是(通过Oracle) V $ SQL 视图。使用连接池用户过滤PARSING_SCHEMA_NAME并检查SQL_TEXT和执行计数。

应该避免两种极端情况:

  • 在查询文本中传递参数(ID)(这是众所周知的)和
  • 重用不同访问路径的语句。

后者的一个示例是使用提供的参数执行对表的有限部分的索引访问的查询,而不使用该参数应处理所有记录(全表扫描)。在这种情况下,创建两个不同的语句肯定没有问题(因为两者的解析都会导致不同的执行计划)。

答案 2 :(得分:1)

PreparedStatements是首选,因为无论您是否以编程方式创建一个,都需要一个;每次运行查询时,数据库内部都会创建一个 - 以编程方式创建一个只为您提供一个句柄。每次创建和丢弃PreparedStatement与使用Statement相比不会增加太多开销。

数据库需要付出很大努力才能创建一个(语法检查,解析,权限检查,优化,访问策略等)。重用一个绕过后续执行的努力。

不要抛弃它们,而是尝试以可重用的方式编写查询,例如忽略空输入参数:

where someCol = coalesce(?, someCol)

所以如果你将参数设置为null(即“未指定”),则条件成功)

或者如果您绝对必须每次都构建查询,请在构建查询为关键字的PreparedStatements中保留对Map的引用,并在遇到命中时重复使用它们。使用WeakHashMap<String, PreparedStatements>映射实现以防止内存不足。

答案 3 :(得分:0)

  

创建了PreparedStatement,但使用了1次或更少次。准备一次语句并多次重复使用它会更有效

我可以安全地忽略这个警告,它类似于声明一周工作40小时比下一个56小时睡觉,7小时后吃,剩下的就是你的< / em> 空闲时间

每个事件你只需要执行一次 - 你应该执行50次以获得更好的平均值吗?

答案 4 :(得分:0)

就性能而言,仅运行一次的

SQL命令只会浪费在Prepared Statement中发送的数据库资源(内存,处理)。另一方面,不使用Prepared Statement会使应用程序容易受到SQL注入的攻击。<​​/ p>

安全性(防止SQL注入)是否影响性能(准备好的语句仅运行一次)?是的,但是...

但是不应该那样。 Java不选择不实现允许开发人员调用正确的数据库API的接口:只能运行一次且可以正确防止SQL注入的SQL命令!为什么Java不能为该特定任务实现正确的工具?

可能如下:

  • Statement Interface-可以提交不同的SQL命令。 SQL命令的一次执行。不允许绑定变量。
  • PreparedStatement Interface-可以提交一个SQL命令。 SQL命令的多次执行。允许绑定变量。
  • (在JAVA中丢失!)RunOnceStatement-可以提交一个SQL命令。 SQL命令的一次执行。允许绑定变量。

例如,可以通过将驱动程序映射到以下内容来在Postgres中调用正确的例程(API):
-Statement Interface-致电PQExec()
-PreparedStatement Interface-致电PQPrepare() / PQExecPrepare() / ...
-(在JAVA中丢失!)RunOnceStatement Interface-致电PQExecParams()

在SQL代码中使用仅运行一次的预备语句是一个很大的性能问题:通过维护计划(稍后将不再调用),数据库中的更多处理将浪费数据库内存。缓存计划太过拥挤,以至于可以从缓存中删除多次执行的实际SQL命令。

但是Java并没有实现正确的接口,并迫使每个人都在各处使用Prepared Statement,只是为了防止SQL注入...