1

I have the following column:

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

which is used to hold a nullable Integer value.

Now I have a row with that column filled. I'm performing a patch using Spring's JdbcTemplate, meaning that I want to update the column only if the new value is not null:

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

This fails with:

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

when I pass a Java null to ?. This means that COALESCE is not happy, since I passed two different types into it. My initial assumption is that Spring/JdbcTemplate somehow does not pass SQL null to the database, because a direct update to the database with:

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

works OK. However, when I replace the query with:

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

I get what I want with JdbcTemplate. What's happening, where's the difference?

Update:

Java code I'm using is as follows:

I have:

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

and 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());
Hasan Can Saral
  • 2,950
  • 5
  • 43
  • 78

2 Answers2

4

From the 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.

From the 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.

You will notice that NVL explicitly states it will perform an implicit conversion so that expr2 is the same data type as expr1 whereas COALESCE (although slightly confusingly worded) does not mention performing an implicit conversion (except for numeric data types) and will expect that all expressions in its argument list are the same data type.

Your query for NVL is effectively converted to:

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

but your COALESCE function is:

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

and expr1 and expr2 have different data types and the query raises an exception.


Assuming that there are no other columns being modified, you do not need to perform an UPDATE if the value is NULL as it is not going to change anything and could re-write your Java code as:

MyEnum enumValue = myClass.getMyEnum();
if ( enumValue != null )
{
  getJdbcTemplate().update(
    "UPDATE my_table SET some_column = ? WHERE...",
    enumValue.getStatus()
  );
}
MT0
  • 143,790
  • 11
  • 59
  • 117
  • It is worth noting there is no good reason to execute this statement when the placeholder's value is null. Oracle does not optimize out that update. It will always update a value to itself which makes no effective change to data. All it does is make the RDBMS busier in multiple ways: (e.g., redo generation, database call execution, dbwr busy-ness) – Jeff Holt May 03 '21 at 15:24
  • I cannot follow how Java `null` becomes `:your_string_bind_variable`. – Hasan Can Saral May 03 '21 at 15:26
  • 1
    @HasanCanSaral I was using a named bind variable `:your_string_bind_variable` rather than an anonymous bind variable `?` to make it clear that there is a different data type (which, by your error message, it is clear that there is). As to why it is passed in the Java call as a string value and not an integer then that is a good question; hopefully someone will be able to dig through the source code of `JdbcTemplate.update` and answer it for you; my answer is intended to answer the question of why there was the difference between the behaviour of the two functions. – MT0 May 03 '21 at 15:36
  • @JeffHolt it would be a *golden feature* if Oracle could optimize this, espetially for updates of many columns including large `CLOB`s where only a part of the columns is realy changing... – Marmite Bomber May 03 '21 at 20:12
1

Rewrite that UPDATE statement explicitely so that you do not change the row at all if the parameter is NULL.

This is from the preformance perspective much better than to update same to same (even such no-update produce undo must be logged.

This will also solve your problem with coalesce (as you do not used it)

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

You must pass the same value twice or use named bind variable.

The root cause of the problem is that you pass the null parameter as a VARCHAR data type.

You may verify it if you run the following statment (note the explicite casting) you get the observed error

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

So the surest way is to explicitely pass the data type of the paramater as well. Here is the example using 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));
Marmite Bomber
  • 19,886
  • 4
  • 26
  • 53
  • This is not going to work for me because I'm updating around 20 columns at once, so it'd be impossible to write it as you suggest. Also, I'm not very concerned performance-wise. – Hasan Can Saral May 03 '21 at 16:53
  • For sure, you'll *not* create 10^20 different statements to get a optimize performance. I added examples with explicit typing of the parameters that resolve the issue. – Marmite Bomber May 03 '21 at 20:06