【问题标题】:Why does NVL work over COALESCE in Oracle?为什么 NVL 在 Oracle 中通过 COALESCE 工作?
【发布时间】:2021-05-03 14:36:52
【问题描述】:

我有以下列:

...
some_column NUMBER(1, 0)  DEFAULT NULL NULL,
...

用于保存可以为空的Integer 值。

现在我有一行填充了该列。我正在使用 Spring 的 JdbcTemplate 执行补丁,这意味着我只想在新值不为空时更新列:

UPDATE my_table SET some_column = COALESCE(?, some_column) WHERE...

这失败了:

Caused by: java.sql.SQLSyntaxErrorException: ORA-00932: inconsistent datatypes: expected CHAR got NUMBER

当我将 Java null 传递给 ? 时。这意味着COALESCE 不高兴,因为我向其中传递了两种不同的类型。我最初的假设是 Spring/JdbcTemplate 不知何故不会将 SQL null 传递给数据库,因为直接更新到数据库:

UPDATE my_table SET some_column = COALESCE(null, some_column) WHERE...

工作正常。但是,当我将查询替换为:

UPDATE my_table SET some_column = NVL(?, some_column) WHERE...

我通过JdbcTemplate 得到我想要的。这是怎么回事,区别在哪里?

更新:

我使用的Java代码如下:

我有:

public class MyClass {
    private MyEnum enum;
    // Getters and setters
}

MyEnum:

public enum MyEnum implements Serializable {
    SOME_VAL (0),
    SOME_OTHER_VAL (1),
    
    ...

    private final int status;

    MyEnum (int status) {
        this.status = status;
    }

    public int getStatus() {
        return status;
    }

getJdbcTemplate().update("UPDATE my_table SET some_column = COALESCE(?, some_column) WHERE...", myClass.getMyEnum() == null ? null : myClass.getMyEnum().getStatus());

【问题讨论】:

  • 听起来您将值绑定为字符串,而不是数字;这会产生这种效果。 (这两个函数的文档显示了不同的隐式数据类型行为,但您可以see it here。)您如何提供绑定值?
  • edit您的问题并添加您正在使用的Java代码。
  • @a_horse_with_no_name 我刚刚编辑了我的问题以显示 Java 代码。
  • @AlexPoole 我刚刚编辑了我的问题以显示 Java 代码。
  • 当占位符具有非空值和空值时,NVL 的使用是否有效?

标签: oracle spring-boot jdbctemplate coalesce nvl


【解决方案1】:

来自NVL documentation

参数expr1expr2 可以具有任何数据类型。如果它们的数据类型不同,那么 Oracle 数据库会隐式地将一种转换为另一种。如果它们不能被隐式转换,那么数据库会返回一个错误。隐式转换实现如下:

  • 如果expr1是字符数据,那么Oracle数据库在比较它们之前将expr2转换为expr1的数据类型,并以expr1的字符集返回VARCHAR2
  • 如果 expr1 是数字,则 Oracle 数据库确定哪个参数具有最高的数字优先级,将另一个参数隐式转换为该数据类型,并返回该数据类型。

来自COALESCE documentation

Oracle 数据库使用短路评估。数据库评估每个 expr 值并确定它是否为 NULL,而不是在确定其中任何一个值是否为 NULL 之前评估所有 expr 值。

如果所有出现的 expr 都是数值数据类型或任何可以隐式转换为数值数据类型的非数值数据类型,则 Oracle 数据库确定具有最高数值优先级的参数,并将其余参数隐式转换为该数据类型,并返回该数据类型。

您会注意到NVL 明确声明它将执行隐式转换,因此expr2expr1 的数据类型相同,而COALESCE(虽然措辞有些混乱)没有提及执行隐式转换(除了数字数据类型),并期望其参数列表中的所有表达式都是相同的数据类型。

您对NVL 的查询已有效转换为:

UPDATE my_table
SET    some_column = CAST(
                       NVL(
                         :your_string_bind_variable,
                         CAST( some_column AS VARCHAR2 )
                       )
                       AS NUMBER
                     )
WHERE...

但你的COALESCE 函数是:

UPDATE my_table
SET    some_column = COALESCE(
                       :your_string_bind_variable,
                       some_column
                     )
WHERE...

expr1expr2 具有不同的数据类型,查询会引发异常。


假设没有其他列被修改,如果值为NULL,则不需要执行UPDATE,因为它不会改变任何内容,并且可以将您的Java代码重写为:

MyEnum enumValue = myClass.getMyEnum();
if ( enumValue != null )
{
  getJdbcTemplate().update(
    "UPDATE my_table SET some_column = ? WHERE...",
    enumValue.getStatus()
  );
}

【讨论】:

  • 值得注意的是,当占位符的值为空时,没有充分的理由执行此语句。 Oracle 不会优化该更新。它总是会更新自己的值,而不会对数据进行有效更改。它所做的只是以多种方式使 RDBMS 更加忙碌:(例如,重做生成、数据库调用执行、dbwr 忙碌)
  • 我无法理解 Java null 是如何变成 :your_string_bind_variable 的。
  • @HasanCanSaral 我使用命名绑定变量 :your_string_bind_variable 而不是匿名绑定变量 ? 来明确表示存在不同的数据类型(根据您的错误消息,它是清楚有)。至于为什么它在 Java 调用中作为字符串值而不是整数传递,那么这是一个好问题;希望有大神能翻翻JdbcTemplate.update的源码为你解答;我的回答旨在回答为什么两个函数的行为之间存在差异的问题。
  • @JeffHolt 如果 Oracle 可以对此进行优化,这将是一个黄金特性,尤其是对于许多列的更新,包括大型 CLOBs,其中只有一部分列是真实的改变...
【解决方案2】:

重写UPDATE 语句明确,这样如果参数为NULL,您根本不会更改行。

从性能的角度来看,这比 update same to same 好得多(即使这样 no-update 产生 undo 必须是 记录

这也将解决您使用coalesce 的问题(因为您没有使用它)

UPDATE my_table SET some_column =  ?  
WHERE ? is not NULL /* do NOT update if the new value is NULL */
and ...

您必须两次传递相同的值或使用命名绑定变量。

问题的根本原因是您将null 参数作为VARCHAR 数据类型传递。

如果您运行以下语句(注意显式转换),您可以验证它,您会得到观察到的错误

update tab
set some_column = COALESCE(cast (null as varchar2(10)), some_column) WHERE id = 1
;
-- SQL Error: ORA-00932: inconsistent datatypes: expected CHAR got NUMBER

所以最可靠的方法是显式传递参数的数据类型。这是使用SqlParameterValue的示例

String sql = """update tab
set some_column = COALESCE(?, some_column) WHERE id = 1""";

// this fails
def updCnt = jdbcTemplate.update(sql, new SqlParameterValue(Types.VARCHAR,null));

// this works fine
def updCnt = jdbcTemplate.update(sql, new SqlParameterValue(Types.INTEGER,null));

【讨论】:

  • 这对我不起作用,因为我一次要更新大约 20 列,所以不可能按照您的建议编写它。另外,我不太关心性能方面的问题。
  • 当然,您不会创建 10^20 条不同的语句来优化性能。我添加了明确键入解决问题的参数的示例。
猜你喜欢
  • 2010-10-31
  • 2022-06-15
  • 1970-01-01
  • 2018-01-25
  • 1970-01-01
  • 2010-10-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多