单次执行后关闭 PreparedStatement——这是设计缺陷吗?
Closing a PreparedStatement after a single execute – is it a design flaw?
我调查了很多地方,听到了很多可疑的说法,从 PreparedStatement
应该比 Statement
更受欢迎,即使只是为了性能优势;一直声称 PreparedStatement
s 应该专门用于批处理语句而不是其他任何东西。
但是,我关注的(主要是在线的)讨论似乎存在盲点。让我展示一个具体的场景。
我们有一个带有数据库连接池的 EDA 设计的应用程序。事件来了,有的需要坚持,有的不需要。有些是人为生成的(例如 update/reset 每 X 分钟生成一次)。
一些事件按顺序发生和处理,但其他类型的事件(也需要持久性)可以(并且将会)同时处理。
除了那些人为生成的事件之外,需要持久化的事件如何到达是没有结构的。
此应用程序是很久以前(大约 2005 年)设计的,支持多种 DBMS。典型的事件处理程序(需要持久化的地方):
- 从池中获取连接
- 准备sql声明
- 执行准备好的语句
- 处理结果集,如果适用,关闭它
- 关闭准备好的语句
- 如有必要,准备不同的声明并以相同的方式处理
- return 连接到池
如果一个事件需要批处理,语句准备一次,使用addBatch
/executeBatch
方法。这是一个明显的性能优势,这些情况与此问题无关。
最近,我收到了一个意见,即准备(解析)一条语句、执行一次并关闭的整个想法本质上是对 PreparedStatement
的误用,提供了零性能优势,无论是使用服务器还是客户端准备语句,典型的 DBMS(Oracle、DB2、MSSQL、MySQL、Derby 等)甚至不会将这样的语句提升到准备语句缓存(或者至少,他们的默认 JDBC driver/datasource 不会)。
此外,我不得不在 MySQL 的开发环境中测试某些场景,Connector/J usage analyzer 似乎同意这个想法。对于所有非批处理的准备语句,调用 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 语句听起来是一个糟糕的选择。
有人可以进一步详细说明吗? 逻辑是否 "prepare-execute (once)-close" 有缺陷 并且基本上不鼓励?
P.S。为 Connector/J 明确指定 useUsageAdvisor=true
和 cachePrepStmts=true
并使用 useServerPrepStmts=true
或 useServerPrepStmts=false
仍然会导致在 [=10] 上调用 close()
时关于效率的警告=] 每个 非批处理 SQL 语句的实例。
PreparedStatements
更可取,因为无论您是否以编程方式创建,都需要一个;在内部,每次查询 运行 时,数据库都会创建一个 - 以编程方式创建一个只是给你一个句柄。每次创建和丢弃 PreparedStatement
并不会比使用 Statement
.
增加太多开销
数据库需要付出很大的努力才能创建一个(语法检查、解析、权限检查、优化、访问策略等)。重用一个绕过后续执行的这项工作。
与其丢弃它们,不如尝试以可以重用的方式编写查询,例如忽略空输入参数:
where someCol = coalesce(?, someCol)
所以如果将参数设置为null
(即“未指定”),则条件成功)
或者,如果您绝对必须每次都构建查询,请在 Map
中保留对 PreparedStatements
的引用,其中构建的查询是关键,如果您获得成功,请重用它们。使用 WeakHashMap<String, PreparedStatements>
映射实现以防止 运行ning 内存不足。
Is the logic prepare-execute [once]-close flawed and essentially discouraged?
我不认为这是个问题,本身。给定的 SQL 语句在某些时候需要是 "prepared",无论是显式(使用 PreparedStatement)还是 "on the fly"(使用语句)。如果我们使用 PreparedStatement 而不是 Statement 来处理只会执行一次的内容,可能会产生更多的开销,但所涉及的开销不太可能很大,特别是如果您引用的语句是正确的:
typical DBMSes (Oracle, DB2, MSSQL, MySQL, Derby, etc.) will not even promote such a statement to prepared statement cache (or at least, their default JDBC driver/datasource will not).
不鼓励的是这样的模式:
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 with resources" ...
try (PreparedStatement ps = conn.prepareStatement(" {some constant SQL statement} ")) {
for (int thing : thingList) {
ps.setInt(1, thing);
ps.executeUpdate();
}
}
请注意,即使不使用批处理也是如此。 SQL 语句仍然只准备一次,多次使用。
正如其他人已经指出的那样,最昂贵的部分是解析数据库中的语句。如果语句已经在共享池中解析,一些数据库系统(这在很大程度上依赖于数据库——我将主要针对 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)(这是众所周知的)和
- 为不同的访问路径重用语句。
后者的一个示例是使用提供的参数对 table 的有限部分执行索引访问的查询,而如果没有参数,则应处理所有记录(完整 table扫描)。在那种情况下,创建两个不同的语句绝对没有问题(因为对两者的解析会导致不同的执行计划)。
PreparedStatement created, but used 1 or fewer times. It is more efficient to prepare statements once, and re-use them many times
我想你可以安全地忽略这个警告,它类似于一个声明 每周工作 40 小时比接下来的 56 小时睡觉,7 小时后吃饭和休息是您的 空闲时间。
每个事件您只需要执行一次 - 您应该执行 50 次以获得更好的平均值吗?
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注入...
我调查了很多地方,听到了很多可疑的说法,从 PreparedStatement
应该比 Statement
更受欢迎,即使只是为了性能优势;一直声称 PreparedStatement
s 应该专门用于批处理语句而不是其他任何东西。
但是,我关注的(主要是在线的)讨论似乎存在盲点。让我展示一个具体的场景。
我们有一个带有数据库连接池的 EDA 设计的应用程序。事件来了,有的需要坚持,有的不需要。有些是人为生成的(例如 update/reset 每 X 分钟生成一次)。 一些事件按顺序发生和处理,但其他类型的事件(也需要持久性)可以(并且将会)同时处理。
除了那些人为生成的事件之外,需要持久化的事件如何到达是没有结构的。
此应用程序是很久以前(大约 2005 年)设计的,支持多种 DBMS。典型的事件处理程序(需要持久化的地方):
- 从池中获取连接
- 准备sql声明
- 执行准备好的语句
- 处理结果集,如果适用,关闭它
- 关闭准备好的语句
- 如有必要,准备不同的声明并以相同的方式处理
- return 连接到池
如果一个事件需要批处理,语句准备一次,使用addBatch
/executeBatch
方法。这是一个明显的性能优势,这些情况与此问题无关。
最近,我收到了一个意见,即准备(解析)一条语句、执行一次并关闭的整个想法本质上是对 PreparedStatement
的误用,提供了零性能优势,无论是使用服务器还是客户端准备语句,典型的 DBMS(Oracle、DB2、MSSQL、MySQL、Derby 等)甚至不会将这样的语句提升到准备语句缓存(或者至少,他们的默认 JDBC driver/datasource 不会)。
此外,我不得不在 MySQL 的开发环境中测试某些场景,Connector/J usage analyzer 似乎同意这个想法。对于所有非批处理的准备语句,调用 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 语句听起来是一个糟糕的选择。
有人可以进一步详细说明吗? 逻辑是否 "prepare-execute (once)-close" 有缺陷 并且基本上不鼓励?
P.S。为 Connector/J 明确指定 useUsageAdvisor=true
和 cachePrepStmts=true
并使用 useServerPrepStmts=true
或 useServerPrepStmts=false
仍然会导致在 [=10] 上调用 close()
时关于效率的警告=] 每个 非批处理 SQL 语句的实例。
PreparedStatements
更可取,因为无论您是否以编程方式创建,都需要一个;在内部,每次查询 运行 时,数据库都会创建一个 - 以编程方式创建一个只是给你一个句柄。每次创建和丢弃 PreparedStatement
并不会比使用 Statement
.
数据库需要付出很大的努力才能创建一个(语法检查、解析、权限检查、优化、访问策略等)。重用一个绕过后续执行的这项工作。
与其丢弃它们,不如尝试以可以重用的方式编写查询,例如忽略空输入参数:
where someCol = coalesce(?, someCol)
所以如果将参数设置为null
(即“未指定”),则条件成功)
或者,如果您绝对必须每次都构建查询,请在 Map
中保留对 PreparedStatements
的引用,其中构建的查询是关键,如果您获得成功,请重用它们。使用 WeakHashMap<String, PreparedStatements>
映射实现以防止 运行ning 内存不足。
Is the logic prepare-execute [once]-close flawed and essentially discouraged?
我不认为这是个问题,本身。给定的 SQL 语句在某些时候需要是 "prepared",无论是显式(使用 PreparedStatement)还是 "on the fly"(使用语句)。如果我们使用 PreparedStatement 而不是 Statement 来处理只会执行一次的内容,可能会产生更多的开销,但所涉及的开销不太可能很大,特别是如果您引用的语句是正确的:
typical DBMSes (Oracle, DB2, MSSQL, MySQL, Derby, etc.) will not even promote such a statement to prepared statement cache (or at least, their default JDBC driver/datasource will not).
不鼓励的是这样的模式:
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 with resources" ...
try (PreparedStatement ps = conn.prepareStatement(" {some constant SQL statement} ")) {
for (int thing : thingList) {
ps.setInt(1, thing);
ps.executeUpdate();
}
}
请注意,即使不使用批处理也是如此。 SQL 语句仍然只准备一次,多次使用。
正如其他人已经指出的那样,最昂贵的部分是解析数据库中的语句。如果语句已经在共享池中解析,一些数据库系统(这在很大程度上依赖于数据库——我将主要针对 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)(这是众所周知的)和
- 为不同的访问路径重用语句。
后者的一个示例是使用提供的参数对 table 的有限部分执行索引访问的查询,而如果没有参数,则应处理所有记录(完整 table扫描)。在那种情况下,创建两个不同的语句绝对没有问题(因为对两者的解析会导致不同的执行计划)。
PreparedStatement created, but used 1 or fewer times. It is more efficient to prepare statements once, and re-use them many times
我想你可以安全地忽略这个警告,它类似于一个声明 每周工作 40 小时比接下来的 56 小时睡觉,7 小时后吃饭和休息是您的 空闲时间。
每个事件您只需要执行一次 - 您应该执行 50 次以获得更好的平均值吗?
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注入...