为什么 NVL 在 Oracle 中优于 COALESCE?

Why does NVL work over COALESCE in Oracle?

我有以下列:

...
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());

来自NVL documentation

The arguments expr1 and expr2 can have any data type. If their data types are different, then Oracle Database implicitly converts one to the other. If they cannot be converted implicitly, then the database returns an error. The implicit conversion is implemented as follows:

  • If expr1 is character data, then Oracle Database converts expr2 to the data type of expr1 before comparing them and returns VARCHAR2 in the character set of expr1.
  • If expr1 is numeric, then Oracle Database determines which argument has the highest numeric precedence, implicitly converts the other argument to that data type, and returns that data type.

来自COALESCE documentation:

Oracle Database uses short-circuit evaluation. The database evaluates each expr value and determines whether it is NULL, rather than evaluating all of the expr values before determining whether any of them is NULL.

If all occurrences of expr are numeric data type or any non-numeric data type that can be implicitly converted to a numeric data type, then Oracle Database determines the argument with the highest numeric precedence, implicitly converts the remaining arguments to that data type, and returns that data type.

您会注意到 NVL 明确声明它将执行隐式转换,因此 expr2 是与 expr1 相同的数据类型,而 COALESCE (虽然有点令人困惑worded) 没有提到执行隐式转换(数字数据类型除外)并且期望其参数列表中的所有表达式都是相同的数据类型。

您对 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有不同的数据类型,查询引发异常。


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

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

重写 UPDATE 语句 显式 以便在 参数为 NULL 时根本不更改该行。

从性能的角度来看,这比 将相同更新为相同 要好得多(即使这样 no-update 也会产生 undo 必须 logged.

这也将解决您使用 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));