Strange result of UPDATE..WHERE..EXISTS with ORA_ROWSCN as join condition

Viewed 89

Here is a quick demo to show the case:

1. Create table

DROP TABLE ora_rowscn_test CASCADE CONSTRAINTS;

CREATE TABLE ora_rowscn_test(
    row_id NUMBER PRIMARY KEY,
    row_value NUMBER NOT NULL
)
ROWDEPENDENCIES;

Note that the table is created with ROWDEPENDENCIES flag, so two rows in same block can have different ROWSCN.

2. Prepare data

BEGIN
    delete ORA_ROWSCN_TEST;
    
    INSERT INTO ORA_ROWSCN_TEST (ROW_ID,ROW_VALUE) VALUES ( 1, 1 );
    commit;
    INSERT INTO ORA_ROWSCN_TEST (ROW_ID,ROW_VALUE) VALUES ( 2, 2 );
--    commit;
    INSERT INTO ORA_ROWSCN_TEST (ROW_ID,ROW_VALUE) VALUES ( 3, 3 );
    commit;
END;
/

Note that commit matters here. If multiple rows are updated in one transaction, they will get same ORA_ROWSCN, otherwise, ORA_ROWSCNs will be different. This behavior seems correct according to Oracle Doc https://docs.oracle.com/cd/B19306_01/server.102/b14200/pseudocolumns007.htm, .

It is not absolutely precise, because Oracle tracks SCNs by transaction committed for the block in which the row resides.

3. Check ORA_ROWSCN of rows

/*******************************************************************************
Since we updated row 1 and 2 in two seperate transactions, their ORA_ROWSCN is different, 
but row 2 and row 3 share same ORA_ROWSCN.

1116438854 1 1
1116438857 2 2
1116438857 3 3
*******************************************************************************/
select ort.ora_rowscn, ort.* from ora_rowscn_test ort;

4. Update values

DECLARE
    lv_number_of_rows      integer;
    lv_test_method         integer := 1;  -- 1:exists, 2:merge, 3:in
    lv_first_rowscn        number := 1116438854;  -- SCN of row 1
    lv_second_rowscn       number := 1116438857;  -- SCN of row 2
    
BEGIN

    if lv_test_method = 1 then

        UPDATE ora_rowscn_test ort
           SET ROW_VALUE = dbms_random.random
         WHERE EXISTS (
                 SELECT 1
                   FROM (
                          SELECT 1 row_id, lv_first_rowscn last_ora_rowscn FROM dual 
                           UNION ALL
                          SELECT 2 row_id, lv_second_rowscn last_ora_rowscn FROM dual
                        ) tmp
                  WHERE ort.row_id = tmp.row_id
                    AND ort.ora_rowscn = tmp.last_ora_rowscn
               );
        
    elsif lv_test_method = 2 then
    
        MERGE INTO ora_rowscn_test ort
        USING ( SELECT 1 row_id, lv_first_rowscn last_ora_rowscn FROM dual
                 UNION ALL
                SELECT 2 row_id, lv_second_rowscn last_ora_rowscn FROM dual
              )
        tmp ON ( tmp.last_ora_rowscn = ort.ora_rowscn
           AND tmp.row_id = ort.row_id )
        WHEN MATCHED THEN UPDATE
           SET ROW_VALUE = dbms_random.random;
           
    elsif lv_test_method = 3 then
    
        UPDATE ora_rowscn_test ort
           SET ROW_VALUE = dbms_random.random
         WHERE ( ort.row_id, ort.ora_rowscn ) IN (
                  SELECT 1 row_id, lv_first_rowscn last_ora_rowscn FROM dual
                   UNION ALL
                  SELECT 2 row_id, lv_second_rowscn last_ora_rowscn FROM dual
                );
           
    end if;

    lv_number_of_rows := sql%rowcount;
    dbms_output.put_line('lv_number_of_rows: ' || lv_number_of_rows);
    dbms_output.put_line(case when lv_number_of_rows = 2 then 'SUCCESS' else 'FAILURE' end);
    
    rollback; -- rollback for repeatedly testing
--    commit; -- commit to check result
    
END;
/

Here I tried 3 methods to update rows and they are supposed to be equivalent to each other, at least I thought it is. But UPDATE..WHERE..EXISTS failed when multiple unequal SCNs are joined (e.g. 1116438854 and 1116438857 are joined simultaneously), whereas MERGE and UPDATE..WHERE..IN succeed in both cases.

Cases UDPATE..WHERE..EXISTS MERGE UPDATE..WHERE..IN
Same SCNs success success success
Diff SCNs fail success success

So my question is what makes the difference, exist clause, ORA_ROWSCN or combined?

1 Answers

I believe the exists does not work as you think in the first method.

SQL> DROP TABLE ora_rowscn_test CASCADE CONSTRAINTS;

Table dropped.

SQL> CREATE TABLE ora_rowscn_test(
    row_id NUMBER PRIMARY KEY,
    row_value NUMBER NOT NULL
)
ROWDEPENDENCIES;

Table created.

SQL> BEGIN
  2      delete ORA_ROWSCN_TEST;
  3      INSERT INTO ORA_ROWSCN_TEST (ROW_ID,ROW_VALUE) VALUES ( 1, 1 );
  4      commit;
  5      INSERT INTO ORA_ROWSCN_TEST (ROW_ID,ROW_VALUE) VALUES ( 2, 2 );
  6  --    commit;
  7      INSERT INTO ORA_ROWSCN_TEST (ROW_ID,ROW_VALUE) VALUES ( 3, 3 );
  8      commit;
  9* END;
 10  /

PL/SQL procedure successfully completed.



 SQL> select ort.ora_rowscn, ort.* from ora_rowscn_test ort;

          ORA_ROWSCN     ROW_ID  ROW_VALUE
-------------------- ---------- ----------
      13142311025954          1          1
      13142311025956          2          2
      13142311025956          3          3

The reason why your first update does not work in the way you think it should, it is because you are using EXISTS

The Exists keyword evaluates true or false, but the IN keyword will compare all values in the corresponding subuery column. If you are using the IN operator, the SQL engine will scan all records fetched from the inner query. On the other hand, if we are using EXISTS, the SQL engine will stop the scanning process as soon as it found a match.

That is why your update ends with one row. Both methods, 2 and 3 use in operators which are evaluating the whole inner query. The method 1 will not update all records concerning the EXIST condition, even if you use the whole data set.

Demo

SQL> DECLARE
  2      lv_number_of_rows      integer;
  3      lv_test_method         integer := 1;
  4      lv_first_rowscn        number  := 13142311025954;  -- SCN of row 1
  5      lv_second_rowscn       number  := 13142311025956;  -- SCN of row 2
  6  BEGIN
  7      if lv_test_method = 1
  8     then
  9          UPDATE ora_rowscn_test ort
 10             SET ROW_VALUE = dbms_random.random
 11           WHERE EXISTS (
 12                   SELECT 1
 13                     FROM (
 14                            SELECT 1 row_id, lv_first_rowscn last_ora_rowscn FROM dual
 15                             UNION ALL
 16                            SELECT 2 row_id, lv_second_rowscn last_ora_rowscn FROM dual
 17                                                UNION ALL
 18                                               SELECT 3 row_id, lv_second_rowscn last_ora_rowscn FROM dual
 19                          ) tmp
 20                    WHERE ort.row_id = tmp.row_id
 21                      AND ort.ora_rowscn = tmp.last_ora_rowscn
 22                 );
 23     end if;
 24      lv_number_of_rows := sql%rowcount;
 25      dbms_output.put_line('lv_number_of_rows: ' || lv_number_of_rows);
 26      dbms_output.put_line(case when lv_number_of_rows = 2 then 'SUCCESS' else 'FAILURE' end);
 27      rollback; -- rollback for repeatedly testing
 28  --    commit; -- commit to check result
 29* END;
SQL> /
lv_number_of_rows: 2
SUCCESS

PL/SQL procedure successfully completed.

SQL>

UPDATE

Let's deep down in the update of the method 1.

SQL> select ort.ora_rowscn, ort.* from ora_rowscn_test ort

        ORA_ROWSCN     ROW_ID  ROW_VALUE
------------------ ---------- ----------
    13142311051553          1          1
    13142311051555          2          2
    13142311051555          3          3

SQL> UPDATE ora_rowscn_test ort
  2             SET ROW_VALUE = dbms_random.random
  3           WHERE EXISTS (
  4                   SELECT 1
  5                     FROM (
  6                            SELECT 1 row_id, 13142311051553 last_ora_rowscn FROM dual
  7                             UNION ALL
  8                            SELECT 2 row_id, 13142311051555 last_ora_rowscn FROM dual
  9                                                UNION ALL
 10                                               SELECT 3 row_id, 13142311051555 last_ora_rowscn FROM dual
 11                          ) tmp
 12                    WHERE ort.row_id = tmp.row_id
 13*                     AND ort.ora_rowscn = tmp.last_ora_rowscn )
SQL> /

2 rows updated.

SQL> select ora_rowscn , ort.* from ora_rowscn_test ort ;

        ORA_ROWSCN     ROW_ID  ROW_VALUE
------------------ ---------- ----------
    13142311051553          1          1
                            2  552186282
                            3 1778577911

What happens here:

  • The select 1 with the where condition only affects the first iteration of the exists condition
  • As soon as the exists find a match, it ends
  • That is why you get two rows updated.

If you run instead any variation ( methods 2, 3 ) or the one below, you will see how it works.

SQL> UPDATE ora_rowscn_test ort
           SET ROW_VALUE = 
           ( with x as 
           ( SELECT 1 row_id, 13142311051553 last_ora_rowscn, 1 row_value FROM dual 
             UNION ALL
             SELECT 2 row_id, 13142311051555 last_ora_rowscn, 2 row_value FROM dual
             UNION ALL 
             SELECT 3 row_id, 13142311051555 last_ora_rowscn, 3 row_value FROM dual
           ) select dbms_random.value from x where ort.row_id = x.row_id and ort.ora_rowscn = x.last_ora_rowscn
             and ort.row_value = x.row_value 
           )
           ;

3 rows updated.

SQL> select ora_rowscn , ort.* from ora_rowscn_test ort ;

        ORA_ROWSCN     ROW_ID  ROW_VALUE
------------------ ---------- ----------
                            1 .017441695
                            2 .802508693
                            3 .912092903

However, having said that, the only way to use exists in the way you want it, is to use it inside a join with the parent table.

SQL> UPDATE ora_rowscn_test ort SET ROW_VALUE = dbms_random.random
WHERE EXISTS ( select 1 from 
                   (
                      SELECT 1 row_id, 13142311051553 last_ora_rowscn, 1 row_value FROM dual
                      UNION ALL
                      SELECT 2 row_id, 13142311051555 last_ora_rowscn, 2 row_value FROM dual
                      UNION ALL
                      SELECT 3 row_id, 13142311051555 last_ora_rowscn, 3 row_value FROM dual
                    ) tmp
                       join ( select ora_rowscn , ort.* from ora_rowscn_test ort ) ort 
                       on  ort.row_id = tmp.row_id
                       AND ort.ora_rowscn = tmp.last_ora_rowscn 
                       AND ort.row_value = tmp.row_value 
                   );

3 rows updated.

SQL> select ora_rowscn , ort.* from ora_rowscn_test ort ;

        ORA_ROWSCN     ROW_ID  ROW_VALUE
------------------ ---------- ----------
                            1 1175024608
                            2 -2.147E+09
                            3 -1.361E+09
Related