【问题标题】:Closing a PreparedStatement after a single execute – is it a design flaw?单次执行后关闭 PreparedStatement——这是设计缺陷吗?
【发布时间】:2016-01-09 00:26:27
【问题描述】:

我调查了很多地方,听到了很多可疑的说法,从 PreparedStatement 应该优先于 Statement 在任何地方,即使只是为了性能优势;一直声称PreparedStatements 应该专门用于批处理语句而不是其他任何东西。

但是,我所关注的(主要是在线)讨论似乎存在盲点。让我介绍一个具体的场景。


我们有一个带有数据库连接池的 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 语句听起来是一个糟糕的选择。

有人可以详细说明一下吗? “准备-执行(一次)-关闭”的逻辑是否存在缺陷并且基本上不鼓励?

附:为 Connector/J 显式指定 useUsageAdvisor=truecachePrepStmts=true 并使用 useServerPrepStmts=trueuseServerPrepStmts=false 仍然会导致在对 PreparedStatement 实例调用 close() 时为 every批处理 SQL 语句。

【问题讨论】:

  • 传递给PreparedStatement的参数,是从哪里来的?很多时候,PreparedStatement 用于避免基于用户输入构建语句,这可能会破坏语句的结构(例如 SQL 注入)。
  • 是的,我知道自动输入清理。主要是(整数)PK 随事件到达;有时原始输入(二进制数据字段,然后根据字段类型解析并使用setXxx 插入),否则setXxxs 可以使用全局或会话(上述顺序事件)变量调用,[单方面] 由应用程序。尽管如此,问题是是否不鼓励单执行然后关闭逻辑,而不管可能的副作用。
  • 这完全取决于数据库系统和驱动程序。话虽如此,大多数驱动程序(和池)即使在您关闭它们时也会进行语句缓存。因此,除非您有一个长时间运行的处理线程(actor 样式),否则您希望遵循关闭 PS 的模式(因为当您返回连接时,池无论如何都会这样做)。保持 PS 打开的最大问题是,它们仅适用于当前连接,因此您还需要保持连接。

标签: java jdbc eda


【解决方案1】:

就性能而言,只运行一次的 SQL 命令只会浪费在准备好的语句中发送的数据库资源(内存、处理)。另一方面,不使用 Prepared Statement 会让应用容易受到 SQL 注入的攻击。

安全性(防止 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注入...

【讨论】:

  • 请注意,虽然 PS 对象在事件处理程序代码中被使用一次,但这并不意味着计划被浪费了。随着更多事件的到来,该计划将被重复使用。问题是关于是否有一个 sn-p,其中创建和执行 PS 是否存在缺陷,完全不管 sn-p 执行的频率(是的,在这种情况下经常执行)。
【解决方案2】:

PreparedStatement 已创建,但使用了 1 次或更少。一次准备语句,多次重复使用,效率更高

我觉得你可以放心地忽略这个警告,它类似于索赔 一周中的前 40 小时工作比接下来 56 小时睡觉、7 小时后吃东西更有效率,其余的都是你的 空闲时间

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

【讨论】:

  • 其实这很合理。
【解决方案3】:

正如其他人已经说过的,最昂贵的部分是解析数据库中的语句。如果语句已经在共享池中解析,一些数据库系统(这几乎取决于数据库——我将主要针对 Oracle 发言)可能会受益。 (在 Oracle 术语中,这称为 软解析,它比 硬解析 - 新语句的解析更便宜)。即使只使用一次准备好的语句,您也可以从软解析中获益。

所以重要的任务是给数据库一个重用语句的机会。一个典型的反例是在 Hibernate 中基于集合处理 IN 列表。你以这样的语句结束

 .. FROM T WHERE X in (?,?,?,  … length based on the size of the collection,?,? ,?,?)

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

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

应避免两种极端情况:

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

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

【讨论】:

  • 好吧,我很高兴听到 PS 重新解析至少依赖于实现。再加上应用程序支持多个 DBMS 和您的其他答案这一事实,当前的方法似乎是最佳的。
【解决方案4】:

准备-执行 [once]-close 的逻辑是否存在缺陷并且基本上不鼓励?

我不认为这是一个问题,本身。给定的 SQL 语句需要在某个时候“准备”,无论是显式(使用 PreparedStatement)还是“即时”(使用 Statement)。如果我们使用 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 语句仍然只准备一次,多次使用。

【讨论】:

  • 实际上对于这样的事情,我们使用连接元数据来查看是否启用了批处理,获取禁用自动提交的连接并使用addBatch()而不是executeUpdate()并在关闭PS之前执行executeBatch() ,然后在关闭连接之前commit()。当然,如果元数据说不支持批处理,我们就回退到这里描述的逻辑。
【解决方案5】:

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

创建数据库需要付出很大的努力(语法检查、解析、权限检查、优化、访问策略等)。重用一个会绕过这项工作以进行后续执行。

与其丢弃它们,不如尝试以可以重复使用的方式编写查询,例如忽略空输入参数:

where someCol = coalesce(?, someCol)

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

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

【讨论】:

  • 它确实为数据库增加了额外的网络往返,因为应用程序必须等待语句句柄的响应才能发送命令来执行它。不是大问题,但值得牢记。
  • @Wyzard 很好,但通常查询非常小(一帧),尤其是与结果集相比,并且数据库通常在网络中“靠近”,而且无论如何,数据库查询交互都非常健谈,因此在大多数情况下甚至可能不会注意到额外的往返。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-15
  • 1970-01-01
  • 2015-01-08
  • 2011-05-06
  • 2013-01-10
  • 2011-07-11
相关资源
最近更新 更多