绑定参数使速度变慢,使用JdbcTemplate速度更慢

5

我在我的Oracle 11g数据库中有一个实现扩展表反模式的四列表。我注意到一些查询花费了很长时间,努力创建更好的索引;在交互会话中是可以的,但使用Spring的NamedJdbcTemplate仍然很慢。

考虑以下例程:

private void getObjectIds(ObjectDomain domain, HashMap<String, List<String>> dimensionMap)
    throws SQLException {
String sql = "SELECT m2.OBJECT_ID"
    + "  FROM MetaInformations m1, MetaInformations m2\n"
    + "  WHERE m1.OBJECT_ID = m2.OBJECT_ID\n"
    + "    AND m1.OBJECT_DOMAIN = :domain AND m1.KEY = :key1 AND\n"
    + "        m1.REF_OBJ_VALUE IN (:values1)\n"
    + "    AND m2.OBJECT_DOMAIN = :domain AND m2.KEY = :key2 AND\n"
    + "        m2.REF_OBJ_VALUE IN (:values2)";
String sqlWithBind = "SELECT m2.OBJECT_ID\n"
    + "  FROM MetaInformations m1, MetaInformations m2\n"
    + "  WHERE m1.OBJECT_ID = m2.OBJECT_ID\n"
    + "    AND m1.OBJECT_DOMAIN = ? AND m1.KEY = ? AND\n"
    + "        m1.REF_OBJ_VALUE IN (?, ?, ?, ?)\n"
    + "    AND m2.OBJECT_DOMAIN = ? AND m2.KEY = ? AND\n"
    + "        m2.REF_OBJ_VALUE IN (?)";

// Prebuilding statement, no bind variables left
Stopwatch stopWatch2 = Stopwatch.createStarted();
Iterator<Entry<String, List<String>>> entries = dimensionMap.entrySet().iterator();
Entry<String, List<String>> entry1 = entries.next();
Entry<String, List<String>> entry2 = entries.next();
String prebuilt = sql.replace(":domain", "'" + domain + "'")
    .replace(":key1", "'" + entry1.getKey() + "'")
    .replace(":values1",
        entry1.getValue().stream().map(s -> "'" + s + "'").collect(Collectors.joining(", ")))
    .replace(":key2", "'" + entry2.getKey() + "'")
    .replace(":values2",
        entry2.getValue().stream().map(s -> "'" + s + "'").collect(Collectors.joining(", ")));
Set<Long> rs2 = extractIdSet(getNamedParameterJdbcTemplate().queryForRowSet(prebuilt, Collections.emptyMap()));
log.warn("Prebuilt took: {} ms", stopWatch2.elapsed(TimeUnit.MILLISECONDS));

// Simple JDBCTemplate with 9 bind parameters
Stopwatch stopWatch5 = Stopwatch.createStarted();
Set<Long> rs1 = extractIdSet(getJdbcTemplate().queryForRowSet(sqlWithBind,
    domain.toString(),
    entry1.getKey(),
    entry1.getValue().get(0),
    entry1.getValue().get(1),
    entry1.getValue().get(2),
    entry1.getValue().get(3),
    domain.toString(),
    entry2.getKey(),
    entry2.getValue().get(0)));
log.warn("JdbcTemplate took: {} ms", stopWatch5.elapsed(TimeUnit.MILLISECONDS));

// Most beautiful: NamedJDBCTemplate
Stopwatch stopWatch3 = Stopwatch.createStarted();
Map<String, Object> paramMap = createNamedParameterMap(domain, dimensionMap);
Set<Long> rs3 = extractIdSet(getNamedParameterJdbcTemplate().queryForRowSet(sql, paramMap));
log.warn("NamedParameterJdbcTemplate took: {} ms", stopWatch3.elapsed(TimeUnit.MILLISECONDS));
}

这里是结果。确切的时间因运行而异,但始终保持相同数量级的顺序。
  1. 使用不带任何绑定参数的查询非常快,不到100毫秒。
  2. 使用具有9个绑定变量的Spring的JdbcTemplate,性能下降到缓慢状态,大约需要4秒。
  3. 最后,使用最简单和最灵活的NamedJdbcTemplate与第二种情况一样慢;这至少并不令人惊讶,因为在幕后,NamedJdbcTemplate将使用命名参数替换我的查询,生成等效于第二种情况的内容。
这与连接无关,因为它们都从同一个连接池获取连接。 这似乎不仅仅是queryForRowSet()函数,因为它实际上也是最快速的用例中所使用的。 同理,似乎也与Spring的异常转换或参与正在进行的事务无关,因为这也应该影响第1种情况。
所以最后问题是:为什么在没有绑定参数的情况下,Spring的JdbcTemplate比普通语句要慢得多?

1
你的gist链接显示404错误。请在问题本身中发布代码。 - JB Nizet
代码很长。你真的想把它放在问题中吗?无论如何,我修复了损坏的链接,对此感到抱歉。 - Michael Piefel
是的,这应该在问题中提到。这是这里的规定。我们希望您的问题及其答案即使在2年后,当您的要旨不再存在或已被修改时,仍然可以理解。 - JB Nizet
你正在比较苹果和橙子。你正在测试普通查询执行与查询执行和映射完整结果。为了进行公正的比较,应该做同样的事情并进行相同的比较,不要将已完成一半的工作与全部工作进行比较。 - M. Deinum
你说得对,这是一个不幸的疏忽。获取所有结果,或者简单地更改代码以提取count(*),会导致所有带有绑定参数的变量同样缓慢。已相应地编辑了问题;现在问题变成了“为什么绑定参数很慢”,这是我仍然不太理解的问题(反正这是什么样的优化器呢?),但这并不是新问题。 - Michael Piefel
显示剩余2条评论
1个回答

2
结果表明,既不是JdbcTemplate也不是NamedJdbcTemplate。即使后者速度更快,它也与PreparedStatement对比无关。这只是因为普通语句没有绑定参数。如果查询没有绑定参数,使用原始的JDBC和NamedJdbcTemplate的速度大致相同。
我们的Oracle 11g对于具有9个绑定参数的此查询选择了一个糟糕的执行计划,并坚持使用它,无论实际参数如何。我不知道为什么,也没有真正的数据库管理员可用。
在具有相同数据的PostgreSQL 9.3数据库上进行的测试显示,使用绑定参数和不使用绑定参数的速度相同;使用Ubuntu的默认安装。

我知道这是一个老问题,但你找到了解决方法吗?我在使用Oracle 12c和19c时遇到了类似的问题。也许有一些方法可以给Oracle一些提示,告诉它使用哪个执行计划? - Pasharik
1
我遇到了类似的问题,但只有涉及String类型的参数时才会出现。查询数字参数(如Long id)则很快。 我的 Oracle 数据库中的字符串字段被定义为 VARCHAR。我的数据源配置具有参数 defaultNChar=true,因此每个输入的字符串参数都被视为 NVARCHAR 类型,并且 Oracle 数据库必须在底层执行每个字符串值的转换,因此索引无法使用。 这里是一个解决方法,当使用预编译语句时:((OraclePreparedStatement)ps).setFormOfUse(columnIndex, FORM_CHAR);https://dev59.com/Cl8d5IYBdhLWcg3w6lyM - Pasharik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接