Thursday, August 22, 2019

Oracle 18 New Wait Event: 'index (re)build lock or pin object'

Oracle 18 introduced a new wait event: 'index (re)build lock or pin object' when modifying indexes. For example, one rebuild triggers 4 occurrences, started with one lock_mode / pin_mode being 2, followed by three with mode 3, but only second wait on index (namespace=4):

PARSING IN CURSOR #140684589165328 len=32 dep=0 uid=49 oct=9 lid=49 tim=13635775869273 hv=4012613321 ad='a4693328' sqlid='229yc47rkr7q9'
alter index test_tab#idx rebuild

WAIT #140684589165328: nam='index (re)build lock or pin object' ela= 6 namespace=1 lock_mode=2 pin_mode=2 obj#=-1 tim=13635775869402
WAIT #140684589165328: nam='index (re)build lock or pin object' ela= 7 namespace=4 lock_mode=3 pin_mode=3 obj#=-1 tim=13635775872729
WAIT #140684589165328: nam='index (re)build lock or pin object' ela= 10 namespace=1 lock_mode=3 pin_mode=3 obj#=-1 tim=13635778508162
WAIT #140684589165328: nam='index (re)build lock or pin object' ela= 4 namespace=1 lock_mode=3 pin_mode=3 obj#=-1 tim=13635778508204

  -- Note: namespace=4 is INDEX
The event information is described as:

SQL > select * from v$event_name where name = 'index (re)build lock or pin object';

  EVENT#          : 333
  EVENT_ID        : 3347698104
  NAME            : index (re)build lock or pin object
  PARAMETER1      : namespace
  PARAMETER2      : lock_mode
  PARAMETER3      : pin_mode
  WAIT_CLASS_ID   : 4166625743
  WAIT_CLASS#     : 3
  WAIT_CLASS      : Administrative
  DISPLAY_NAME    : index (re)build lock or pin object
The Lock/pin mode seems referring to those documented in v$libcache_locks:

  Lock/pin mode:
      0 - No lock/pin held
      1 - Null mode
      2 - Share mode
      3 - Exclusive mode
Since this wait event reveals metrics on index operations related to library cache and shared cursors, it can help us understand shared pool mutex activities.

Here a short demo on the number of waits for different index operations.

SQL > drop table test_tab purge;

SQL > create table test_tab as select 1 x from dual; 

SQL > select total_waits from v$system_event where event = 'index (re)build lock or pin object';

    TOTAL_WAITS
    -----------
          20000

-------------------------- create index (4 Waits) --------------------------

SQL > create index test_tab#idx on test_tab(x);

SQL > select total_waits from v$system_event where event = 'index (re)build lock or pin object';

    TOTAL_WAITS
    -----------
          20004

-------------------------- gather index stats (0 Waits) --------------------------

SQL > exec dbms_stats.gather_index_stats('K', 'TEST_TAB#IDX');

SQL > select total_waits from v$system_event where event = 'index (re)build lock or pin object';

    TOTAL_WAITS
    -----------
          20004   
      
-------------------------- rebuild index (4 Waits) --------------------------

SQL > alter index test_tab#idx rebuild;

SQL > select total_waits from v$system_event where event = 'index (re)build lock or pin object';

    TOTAL_WAITS
    -----------
          20008
      
-------------------------- rebuild index online (3 Waits) --------------------------

SQL > alter index test_tab#idx rebuild online;

SQL > select total_waits from v$system_event where event = 'index (re)build lock or pin object';

    TOTAL_WAITS
    -----------
          20011       (Note: rebuild index online increases only 3)
      
-------------------------- rebuild index reverse (4 Waits) --------------------------

SQL > alter index test_tab#idx rebuild reverse;

SQL > select total_waits from v$system_event where event = 'index (re)build lock or pin object';

    TOTAL_WAITS
    -----------
          20015
      
-------------------------- drop index (0 Waits) --------------------------

SQL > drop index test_tab#idx;

SQL > select total_waits from v$system_event where event = 'index (re)build lock or pin object';

    TOTAL_WAITS
    -----------
          20015

Tuesday, August 6, 2019

PDML Disabled on Nonpartitioned IOT

When a parallel instrumented update of Nonpartitioned IOT (Index-Organized Tables) doesn’t execute in parallel, xplan contains a Note:
     PDML disabled because non partitioned or single fragment IOT used
I will try to demonstrate this behavior, but I'm not sure if there exists any documentation about this restriction, and I don't know what means "single fragment".

Note: All tests are done in Oracle 18c.


1. PDML disabled: single fragment IOT used


Run following test code,

SQL > drop table test_iot_tab;

SQL> create table test_iot_tab (id number, sid number, 
       constraint test_iot_tab#p primary key(id)
     ) organization index; 

SQL > insert into test_iot_tab select level, -1 from dual connect by level <= 1e5;

SQL > commit;

SQL > exec dbms_stats.gather_table_stats(null, 'TEST_IOT_TAB');

SQL > set serveroutput off

SQL > update /*+ enable_parallel_dml parallel(t 4) */ test_iot_tab t set sid = sys_context('userenv','sid');

  100000 rows updated. 

SQL > select * from table(dbms_xplan.display_cursor);

  -------------------------------------
  SQL_ID  3zsrt91jd3x6s, child number 2
  -------------------------------------
  update /*+ enable_parallel_dml parallel(t 4) */ test_iot_tab t set sid
  = sys_context('userenv','sid')
  
  Plan hash value: 1206014583
  
  -----------------------------------------------------------------------------------
  | Id  | Operation        | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
  -----------------------------------------------------------------------------------
  |   0 | UPDATE STATEMENT |                |       |       |     5 (100)|          |
  |   1 |  UPDATE          | TEST_IOT_TAB   |       |       |            |          |
  |   2 |   INDEX FULL SCAN| TEST_IOT_TAB#P |   100K|   878K|     5   (0)| 00:00:01 |
  -----------------------------------------------------------------------------------
  
  Note
  -----
     - PDML disabled because non partitioned or single fragment IOT used
  
  19 rows selected.

SQL > select status, ptx, xid, xidusn, xidslot, xidsqn, used_ublk, used_urec, ptx_xidusn, ptx_xidslt, ptx_xidsqn, s.* 
       from v$transaction t, v$px_session s
      where t.ses_addr=s.saddr(+);

 STATUS PTX XID              XIDUSN  XIDSLOT XIDSQN USED_UBLK USED_UREC PTX_XIDUSN PTX_XIDSLT PTX_XIDSQN SADDR SID SERIAL# QCSID QCSERIAL# QCINST_ID SERVER_GROUP SERVER_SET SERVER# DEGREE REQ_DEGREE
 ------ --- ---------------- ------  ------- ------ --------- --------- ---------- ---------- ---------- ----- --- ------- ----- --------- --------- ------------ ---------- ------- ------ ----------
 ACTIVE NO  52000600A3810000 82      6       33187  1311      100000    0          0          0

SQL > select * from v$px_process;

  no rows selected

SQL > commit;

SQL > select * from v$pq_tqstat;

  no rows selected
xplan is noted with PDML disabled because of single fragment IOT used.

v$transaction.ptx displys "NO", indicating no parallel transaction (PTX stands for parent transaction), hence no rows in v$px_session.

Both selects on v$px_process and v$pq_tqstat return no rows.

Note that for PDML, information from v$pq_tqstat is available only after a commit or rollback operation as documented by Oracle V$PQ_TQSTAT.

If v$pq_tqstat returns rows, it is from the previous committed parallel DML, not the current committed serial DML.

v$px_process returns rows from previous and current parallel executions. The non-null sid and serial# are PX process currently in use, or recently in use because PX server process is shut down if it has not been used within certain time interval (probably 5 minutes).

Technical Article Understanding Parallel Execution – Part 2 also lists various problems with view V$PQ_TQSTAT.

With hint: index_ffs, we can make the query part of update run in parallel, but not DML (UPDATE is not inside PX COORDINATOR in xplan).

SQL > set serveroutput off

SQL > update /*+ enable_parallel_dml parallel(t 4) index_ffs(t test_iot_tab#p) */ test_iot_tab t set sid = sys_context('userenv','sid');
  100000 rows updated.
  
SQL > select * from table(dbms_xplan.display_cursor);

  -------------------------------------
  SQL_ID  93s3czmfbquu1, child number 0
  -------------------------------------
  update /*+ enable_parallel_dml parallel(t 4) index_ffs(t
  test_iot_tab#p) */ test_iot_tab t set sid = sys_context('userenv','sid')
  
  Plan hash value: 2045397606
  
  ------------------------------------------------------------------------------------------------------------------------
  | Id  | Operation                | Name           | Rows  | Bytes | Cost (%CPU)| Time     |    TQ  |IN-OUT| PQ Distrib |
  ------------------------------------------------------------------------------------------------------------------------
  |   0 | UPDATE STATEMENT         |                |       |       |    16 (100)|          |        |   |               |
  |   1 |  UPDATE                  | TEST_IOT_TAB   |       |       |            |          |        |   |               |
  |   2 |   PX COORDINATOR         |                |       |       |            |          |        |   |               |
  |   3 |    PX SEND QC (RANDOM)   | :TQ10000       |   100K|   878K|    16   (0)| 00:00:01 |  Q1,00 | P->S | QC (RAND)  |
  |   4 |     PX BLOCK ITERATOR    |                |   100K|   878K|    16   (0)| 00:00:01 |  Q1,00 | PCWC |            |
  |*  5 |      INDEX FAST FULL SCAN| TEST_IOT_TAB#P |   100K|   878K|    16   (0)| 00:00:01 |  Q1,00 | PCWP |            |
  ------------------------------------------------------------------------------------------------------------------------
  
  Predicate Information (identified by operation id):
  ---------------------------------------------------
  
     5 - access(:Z>=:Z AND :Z<=:Z)
  
  Note
  -----
     - Degree of Parallelism is 4 because of table property
     - PDML disabled because non partitioned or single fragment IOT used
  
SQL > commit;  


2. Update by Package DBMS_PARALLEL_EXECUTE


Since PDML on IOT is not allowed, and it is not clear what means "single fragment IOT", one alternative is to use 11gR2 introduced package dbms_parallel_execute to manually update table in parallel.

First, we try the most common usage of this package: create_chunks_by_rowid (Oracle Docu wrote: Index-organized tables are not allowed for create_chunks_by_rowid).

SQL > exec dbms_parallel_execute.drop_task('test_iot_task');

SQL > exec dbms_parallel_execute.create_task (task_name => 'test_iot_task');

SQL> select * from user_parallel_execute_tasks where task_name = 'test_iot_task';

  TASK_NAME      CHUNK_TYPE  STATUS   TABLE_OWNER  TABLE_NAME  NUMBER_COLUMN  TASK_COMMENT  JOB_PREFIX
  -------------  ----------  -------  -----------  ----------  -------------  ------------  ----------
  test_iot_task  UNDECLARED  CREATED

SQL > begin
        dbms_parallel_execute.create_chunks_by_rowid
          (task_name   => 'test_iot_task',
           table_owner => 'K',
           table_name  => 'TEST_IOT_TAB',
           by_row      => true,   -- or false
           chunk_size  => 200);
      end;
      /

      ERROR at line 1:
      ORA-29491: invalid table for chunking
      ORA-06512: at "SYS.DBMS_PARALLEL_EXECUTE", line 25
      ORA-06512: at "SYS.DBMS_PARALLEL_EXECUTE", line 21
      ORA-06512: at "SYS.DBMS_PARALLEL_EXECUTE", line 120
      ORA-06512: at line 2
We got ORA-29491, which says that create_chunks_by_rowid requires physical ROWID, but IOT can only provide logical rowid.
SQL > oerr ora 29491
 29491, 00000, "invalid table for chunking"
 // *Cause:  An attempt was made to chunk a table by ROWID, 
 //          but the table was not a physical table or the table was an IOT.
 //          physical table or the table is an IOT.
 // *Action: Use a table which has physical ROWID.
So we try another chunking method: create_chunks_by_number_col:

SQL > begin
        dbms_parallel_execute.create_chunks_by_number_col
         (task_name    => 'test_iot_task',
          table_owner  => 'K',
          table_name   => 'TEST_IOT_TAB',
          table_column => 'ID',
          chunk_size   => 200);
         end;
         /

      PL/SQL procedure successfully completed.

SQL > select * from user_parallel_execute_tasks where task_name = 'test_iot_task';

  TASK_NAME      CHUNK_TYPE    STATUS   TABLE_OWNER  TABLE_NAME    NUMBER_COLUMN  TASK_COMMENT  JOB_PREFIX
  -------------  ------------  -------  -----------  ------------  -------------  ------------  ----------
  test_iot_task  NUMBER_RANGE  CHUNKED  K            TEST_IOT_TAB  ID
  

SQL > declare
       l_sql_stmt varchar2(32767);
     begin
       l_sql_stmt := q'[update test_iot_tab t 
                        set    t.sid = sys_context('userenv','sid')
                        where id between :start_id and :end_id]';
     
       dbms_parallel_execute.run_task(task_name      => 'test_iot_task',
                                      sql_stmt       => l_sql_stmt,
                                      language_flag  => dbms_sql.native,
                                      parallel_level => 10);
     end;
     /
     
     PL/SQL procedure successfully completed.


SQL > select * from user_parallel_execute_tasks where task_name = 'test_iot_task';

  TASK_NAME      CHUNK_TYPE    STATUS    TABLE_OWNER  TABLE_NAME    NUMBER_COLUMN  TASK_COMMENT  JOB_PREFIX
  -------------  ------------  --------  -----------  ------------  -------------  ------------  ----------
  test_iot_task  NUMBER_RANGE  FINISHED  K            TEST_IOT_TAB  ID                           TASK$_81116
                                             
SQL > select sid, count(*) from test_iot_tab group by sid order by sid;

    SID  COUNT(*)
    ---  --------
     27      7000
    196     15400
    197     13600
    203      6200
    377     12600
    379      4600
    559     10800
    729     13600
    750     10200
    908      6000
    
    10 rows selected.
The above test shows that create_chunks_by_number_col is allowed, and table was updated by 10 parallel jobs.

The drawback of dbms_parallel_execute is that each chunk in each job is committed separately, therefore whole transaction is not atomic as documented on dbms_parallel_execute.run_task:
      This procedure executes the specified statement (sql_stmt) on the chunks in parallel. 
      It commits after processing each chunk. 


3. Partitioned Index-Organized Tables


Since PDML is disabled on Nonpartitioned IOT, we can look the case of Partitioned IOT.

SQL> drop table test_iot_tab_part;

SQL > create table test_iot_tab_part (part number, id number, sid number, 
       constraint test_iot_tab_part#p primary key(part, id)
     ) organization index
       partition by list (part)
        (partition p1 values (1),  
         partition p2 values (2),
         partition p3 values (3),
         partition p4 values (4)
        );

SQL > insert into test_iot_tab_part select mod(level, 4) + 1 part, level, -1 from dual connect by level <= 1e5;

SQL > commit;

SQL > exec dbms_stats.gather_table_stats(null, 'TEST_IOT_TAB_PART');

SQL > set serveroutput off

SQL > update /*+ enable_parallel_dml parallel(t 4) */ test_iot_tab_part t set sid = sys_context('userenv','sid');

     100000 rows updated. 

SQL > select * from table(dbms_xplan.display_cursor);

  -------------------------------------
  SQL_ID  75bsynpc6h5qq, child number 1
  -------------------------------------
  update /*+ enable_parallel_dml parallel(t 4) */ test_iot_tab_part t set
  sid = sys_context('userenv','sid')
  
  Plan hash value: 4043313740
  
  ---------------------------------------------------------------------------------------------------------------------------------------------
  | Id  | Operation                | Name                | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |    TQ  |IN-OUT| PQ Distrib |
  ---------------------------------------------------------------------------------------------------------------------------------------------
  |   0 | UPDATE STATEMENT         |                     |       |       |     2 (100)|          |       |       |        |      |            |
  |   1 |  PX COORDINATOR          |                     |       |       |            |          |       |       |        |      |            |
  |   2 |   PX SEND QC (RANDOM)    | :TQ10000            |   100K|  1171K|     2   (0)| 00:00:01 |       |       |  Q1,00 | P->S | QC (RAND)  |
  |   3 |    UPDATE                | TEST_IOT_TAB_PART   |       |       |            |          |       |       |  Q1,00 | PCWP |            |
  |   4 |     PX PARTITION LIST ALL|                     |   100K|  1171K|     2   (0)| 00:00:01 |     1 |     4 |  Q1,00 | PCWC |            |
  |   5 |      INDEX FULL SCAN     | TEST_IOT_TAB_PART#P |   100K|  1171K|     2   (0)| 00:00:01 |     1 |     4 |  Q1,00 | PCWP |            |
  ---------------------------------------------------------------------------------------------------------------------------------------------
  
  Note
  -----
     - Degree of Parallelism is 4 because of table property
  
  22 rows selected.

SQL > select status, ptx, xid, xidusn, xidslot, xidsqn, used_ublk, used_urec, ptx_xidusn, ptx_xidslt, ptx_xidsqn, s.* 
       from v$transaction t, v$px_session s
      where t.ses_addr=s.saddr(+);

  STATUS PTX XID              XIDUSN XIDSLOT XIDSQN USED_UBLK USED_UREC PTX_XIDUSN PTX_XIDSLT PTX_XIDSQN SADDR            SID SERIAL# QCSID QCSERIAL# QCINST_ID SERVER_GROUP SERVER_SET SERVER# DEGREE REQ_DEGREE
  ------ --- ---------------- ------ ------- ------ --------- --------- ---------- ---------- ---------- ---------------  --- ------- ----- --------- --------- ------------ ---------- ------- ------ ----------
  ACTIVE YES 5600120012830000 86     18      33554  331       25001     98         11         21043      00000000B7BFCF38 203 27206   392   53164     1         1            1          1       4      4
  ACTIVE YES 53001300117B0000 83     19      31505  331       25001     98         11         21043      00000000B62F60B8 750 29571   392   53164     1         1            1          2       4      4
  ACTIVE YES 61001B00DC4C0000 97     27      19676  331       25001     98         11         21043      00000000B7BF5B70 206 58371   392   53164     1         1            1          3       4      4
  ACTIVE YES 5E0019009B6D0000 94     25      28059  331       25001     98         11         21043      00000000B7DB8C78 377 55026   392   53164     1         1            1          4       4      4
  ACTIVE YES 62000B0033520000 98     11      21043  1         1         98         11         21043      00000000B7D94990 392 53164   392

SQL > select * from v$px_process;

  SERV STATUS     PID SPID     SID SERIAL# IS_GV
  ---- --------- ---- ------ ----- ------- -----
  P000 IN USE      49 32017    203   27206 FALSE
  P001 IN USE      58 32019    750   29571 FALSE
  P002 IN USE      61 32021    206   58371 FALSE
  P003 IN USE      62 32023    377   55026 FALSE
  
  4 rows selected.

SQL > commit;

SQL > select * from v$pq_tqstat;

  DFO_NUMBER TQ_ID SERVER_TYP NUM_ROWS BYTES  OPEN_TIME AVG_LATENCY WAITS TIMEOUTS PROCES
  ---------- ----- ---------- -------- ----- ---------- ----------- ----- -------- ------
           1     0 Producer          2   286          0           0     1        0 P003  
           1     0 Producer          2   286          0           0     1        0 P002  
           1     0 Producer          2   286          0           0     1        0 P001  
           1     0 Producer          2   286          0           0     1        0 P000  
           1     0 Consumer          8  1144          0           0    15        2 QC    
  
  5 rows selected.
The above test shows that update on Partitioned IOT is executed in parallel, i.e. PDML enabled.

Oracle performs two-phase commit protocol in PDML (analogue to distributed transactions) and documented in VLDB and Partitioning Guide (Release 12.2) - 8.5.3.5 Transaction Restrictions for Parallel DML:
    To ensure user-level transactional atomicity, the coordinator uses a two-phase commit protocol 
    to commit the changes performed by the parallel process transactions.
Oracle8 Server Migration (A54650_01) - Oracle8 Enhancements has some details:
    Changes to Fixed Views
  
    The following fixed views contain new information about parallel DML:
  
       V$SESSION: This fixed view contains a new column for ENABLE PARALLEL DML mode.
       V$TRANSACTION: The existing column STATUS has two new values, PTX PREPARED and PTX COMMITTED (where PTX stands for parent transaction). 
                     This fixed view also contains new columns (where XID stands for transaction identifier):
             PTX (value YES or NO)
             PTX_XIDUSN
             PTX_XIDSLT
             PTX_XIDSQN
       V$PQ_SESSTAT: This fixed view contains a new row: DML Parallelized.
       V$PQ_SYSSTAT: This fixed view contains a new row: DML Initiated.
In fact, for above PDML example, if we suspend one parallel slave (e.g. P002) before issuing commit in PX COORDINATOR session, v$transaction.status shows that COORDINATOR session and P002 have status: ACTIVE, but all other PX slaves are marked with status: PTX PREPARED.

--- Launch PDML in PX COORDINATOR session

SQL > update /*+ enable_parallel_dml parallel(t 4) */ test_iot_tab_part t set sid = sys_context('userenv','sid');

SQL > select * from v$px_process;

  SERV STATUS           PID SPID                       SID SERIAL# IS_GV
  ---- --------- ---------- ------------------------ ----- ------- -----
  P000 IN USE            49 6950                       206    5492 FALSE
  P001 IN USE            58 6952                       750   22721 FALSE
  P002 IN USE            61 6954                       203   35975 FALSE
  P003 IN USE            62 6956                       377   60192 FALSE
  
  4 rows selected. 


--- Suspend one parallel slave in ORADEBUG session

SQL(oradebug) > oradebug setospid 6954
  Oracle pid: 61, Unix process pid: 6954, image: oracle@testdb (P002)

SQL(oradebug) > oradebug suspend
  Statement processed.


--- return to PX COORDINATOR session, issue commit 

SQL > commit;


--- in a monitor session, show PTX PREPARED phase

SQL (monitor) > select status, ptx, xid, xidusn, xidslot, xidsqn, used_ublk, used_urec, ptx_xidusn, ptx_xidslt, ptx_xidsqn, s.* 
       from v$transaction t, v$px_session s
      where t.ses_addr=s.saddr(+);

  STATUS       PTX XID              XIDUSN XIDSLOT XIDSQN USED_UBLK USED_UREC PTX_XIDUSN PTX_XIDSLT PTX_XIDSQN SADDR            SID SERIAL# QCSID QCSERIAL# QCINST_ID SERVER_GROUP SERVER_SET SERVER# DEGREE REQ_DEGREE
  ------------ --- ---------------- ------ ------- ------ --------- --------- ---------- ---------- ---------- ---------------  --- ------- ----- --------- --------- ------------ ---------- ------- ------ ----------
  PTX PREPARED YES 5C001600566A0000 92     22      27222  331       25001     94         15         28085      00000000B7BF5B70 206 5492    392   53164     1         1            1          1       4      4
  PTX PREPARED YES 5A000400B4680000 90     4       26804  331       25001     94         15         28085      00000000B62F60B8 750 22721   392   53164     1         1            1          2       4      4
  ACTIVE       YES 5500190020780000 85     25      30752  331       25001     94         15         28085      00000000B7BFCF38 203 35975   392   53164     1         1            1          3       4      4
  PTX PREPARED YES 5F000100C0600000 95     1       24768  331       25001     94         15         28085      00000000B7DB8C78 377 60192   392   53164     1         1            1          4       4      4
  ACTIVE       YES 5E000F00B56D0000 94     15      28085  3         3         94         15         28085      00000000B7D94990 392 53164   392
When a DML doesn’t execute in parallel, it is not always obvious as demonstrated by Jonathan's Blog Quiz Night (March 9, 2017) on unused CLOB column.

I did exercise below to understand the Quiz.

SQL > drop table test_unused_lob_col_tab;

SQL > create table test_unused_lob_col_tab (id number, sid number, unused_clob clob);

SQL > alter table test_unused_lob_col_tab set unused (unused_clob);

SQL > select * from user_unused_col_tabs where table_name = 'TEST_UNUSED_LOB_COL_TAB';

  TABLE_NAME                   COUNT
  ----------------------- ----------
  TEST_UNUSED_LOB_COL_TAB          1

-- DBA_TAB_COLUMNS view filters out system-generated hidden columns and invisible columns
SQL > select table_name, column_name, data_type, column_id                                            
      from  dba_tab_columns                                                                                       
      where table_name = 'TEST_UNUSED_LOB_COL_TAB';                                                                
                                                                                                                   
  TABLE_NAME                COLUMN_NAME                    DATA_TYPE   COLUMN_ID                                
  ------------------------- ------------------------------ ---------- ----------                                
  TEST_UNUSED_LOB_COL_TAB   ID                             NUMBER              1                                
  TEST_UNUSED_LOB_COL_TAB   SID                            NUMBER              2      
  
  2 rows selected. 


--DBA_TAB_COLS has 4 different Column ID: 
--  COLUMN_ID           ,Sequence number of the column as created,  NULL when HIDDEN_COLUMN='YES'
--  SEGMENT_COLUMN_ID   ,Sequence number of the column in the segment, NULL when VIRTUAL_COLUMN = 'YES'
--  INTERNAL_COLUMN_ID  ,Internal sequence number of the column, NOT NULL
--  COLLATED_COLUMN_ID  ,Internal sequence number for virtual column generates a collation key (since 18c)

SQL > select column_name, data_type, hidden_column, virtual_column, column_id, segment_column_id, internal_column_id             
    from dba_tab_cols                                                                                          
   where table_name = 'TEST_UNUSED_LOB_COL_TAB';
  
  COLUMN_NAME                DATA_TYPE HIDDEN VIRTUAL COLUMN_ID SEGMENT_COLUMN_ID INTERNAL_COLUMN_ID
  -------------------------- --------- ------ ------- --------- ----------------- ------------------ 
  ID                         NUMBER    NO     NO      1         1                 1
  SID                        NUMBER    NO     NO      2         2                 2
  SYS_C00003_19080610:24:47$ CLOB      YES    NO                3                 3                                                                                       
 
  3 rows selected. 
  

-- Both LOGIC_COLUMN_ID and SEGMENT_COLUMN_ID are displayed.
-- LOGIC_COLUMN_ID is 0 for hidden column.                                                                                                              
SQL > select o.object_name, o.subobject_name, o.object_id, c.name column_name, c.col# logic_column_id, c.segcol# segment_column_id
            ,(case 
                when     bitand(c.property, 32768) = 32768 
                     and bitand(c.property, 1) != 1 
                     and bitand(c.property, 1024) != 1024 
                then 'YES' 
                else 'NO' 
              end)  unused
            ,bitand(c.property, 32768) unused_chk, bitand(c.property, 1) adt_attr, bitand(c.property, 1024) ntab_setid
          --,c.unusablebefore#, c.unusablebeginning#
       from sys.col$ c, dba_objects o
      where c.obj# = o.object_id 
        and object_name = 'TEST_UNUSED_LOB_COL_TAB'
      order by segment_column_id;
  
  OBJECT_NAME             OBJECT_ID COLUMN_NAME                LOGIC_COLUMN_ID   SEGMENT_COLUMN_ID UNUSED UNUSED_CHK ADT_ATTR NTAB_SETID
  ----------------------- --------- -------------------------- ----------------- ----------------- ------ ---------- -------- ----------
  TEST_UNUSED_LOB_COL_TAB 3754451   ID                         1                 1                 NO     0          0        0
  TEST_UNUSED_LOB_COL_TAB 3754451   SID                        2                 2                 NO     0          0        0
  TEST_UNUSED_LOB_COL_TAB 3754451   SYS_C00003_19080610:24:47$ 0                 3                 YES    32768      0        0

    --Note 1: unused column_name seems a concatenation of column_id with unused timestamp.
    --        two columns: unusablebefore#, unusablebeginning# in sys.col$ are empty.
  
    --Note 2: virtual_column has only internal_column_id in dba_tab_cols, its segment_column_id is empty.
    --        In above query of sys.col$, segcol# (segment_column_id) is 0.

SQL > select dbms_metadata.get_ddl('TABLE', 'TEST_UNUSED_LOB_COL_TAB', 'K') from dual;

       CREATE TABLE "K"."TEST_UNUSED_LOB_COL_TAB" 
         ( "ID"  NUMBER, 
           "SID" NUMBER
         ) SEGMENT CREATION DEFERRED 
       PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 
       NOCOMPRESS LOGGING
       TABLESPACE "TEST_USER" 

SQL > set serveroutput off

SQL > update /*+ enable_parallel_dml parallel(t 4) */ test_unused_lob_col_tab t set sid = sys_context('userenv','sid');  

  0 rows updated.

SQL > select * from table(dbms_xplan.display_cursor);

  -------------------------------------
  SQL_ID  82vs0ct3p2nfj, child number 1
  -------------------------------------
  update /*+ enable_parallel_dml parallel(t 4) */ test_unused_lob_col_tab
  t set sid = sys_context('userenv','sid')
  
  Plan hash value: 4086267116
  
  ------------------------------------------------------------------------------------------------------------------------------
  | Id  | Operation             | Name                    | Rows  | Bytes | Cost (%CPU)| Time     |    TQ  |IN-OUT| PQ Distrib |
  ------------------------------------------------------------------------------------------------------------------------------
  |   0 | UPDATE STATEMENT      |                         |       |       |     2 (100)|          |     | |            |
  |   1 |  UPDATE               | TEST_UNUSED_LOB_COL_TAB |       |       |            |          |     | |            |
  |   2 |   PX COORDINATOR      |                         |       |       |            |          |     | |            |
  |   3 |    PX SEND QC (RANDOM)| :TQ10000                |    82 |  1066 |     2   (0)| 00:00:01 |  Q1,00 | P->S | QC (RAND)  |
  |   4 |     PX BLOCK ITERATOR |                         |    82 |  1066 |     2   (0)| 00:00:01 |  Q1,00 | PCWC |            |
  |*  5 |      TABLE ACCESS FULL| TEST_UNUSED_LOB_COL_TAB |    82 |  1066 |     2   (0)| 00:00:01 |  Q1,00 | PCWP |            |
  ------------------------------------------------------------------------------------------------------------------------------
  
  Predicate Information (identified by operation id):
  ---------------------------------------------------
  
     5 - access(:Z>=:Z AND :Z<=:Z)
  
  Note
  -----
     - Degree of Parallelism is 4 because of table property
     - PDML disabled because single fragment or non partitioned table used

SQL > alter table test_unused_lob_col_tab drop unused columns;

SQL > select * from user_unused_col_tabs where table_name = 'TEST_UNUSED_LOB_COL_TAB';

  no rows selected

SQL > update /*+ enable_parallel_dml parallel(t 4) */ test_unused_lob_col_tab t set sid = sys_context('userenv','sid');  

  0 rows updated.

SQL > select * from table(dbms_xplan.display_cursor);

  -------------------------------------------------------------------------------------------------------------------------------
  SQL_ID  82vs0ct3p2nfj, child number 1
  -------------------------------------
  update /*+ enable_parallel_dml parallel(t 4) */ test_unused_lob_col_tab
  t set sid = sys_context('userenv','sid')
  
  Plan hash value: 3773939051
  
  ------------------------------------------------------------------------------------------------------------------------------
  | Id  | Operation             | Name                    | Rows  | Bytes | Cost (%CPU)| Time     |    TQ  |IN-OUT| PQ Distrib |
  ------------------------------------------------------------------------------------------------------------------------------
  |   0 | UPDATE STATEMENT      |                         |       |       |     2 (100)|          |     | |            |
  |   1 |  PX COORDINATOR       |                         |       |       |            |          |     | |            |
  |   2 |   PX SEND QC (RANDOM) | :TQ10000                |    82 |  1066 |     2   (0)| 00:00:01 |  Q1,00 | P->S | QC (RAND)  |
  |   3 |    UPDATE             | TEST_UNUSED_LOB_COL_TAB |       |       |            |          |  Q1,00 | PCWP |            |
  |   4 |     PX BLOCK ITERATOR |                         |    82 |  1066 |     2   (0)| 00:00:01 |  Q1,00 | PCWC |            |
  |*  5 |      TABLE ACCESS FULL| TEST_UNUSED_LOB_COL_TAB |    82 |  1066 |     2   (0)| 00:00:01 |  Q1,00 | PCWP |            |
  ------------------------------------------------------------------------------------------------------------------------------
  
  Predicate Information (identified by operation id):
  ---------------------------------------------------
  
     5 - access(:Z>=:Z AND :Z<=:Z)
  
  Note
  -----
     - Degree of Parallelism is 4 because of table property
If we look carefully the above xplan Note, and that of Nonpartitioned IOT:
     PDML disabled because single fragment or non partitioned table used
     PDML disabled because non partitioned or single fragment IOT used
they are not exact the same, probably dynamically composed according to the parsed statement.

By the way, in Oracle 12c, when set a column unused, it could hit:
  Bug 26965236 : DELETE FROM TSDP_SENSITIVE_DATA$ CAUSING ENQ: TM - CONTENTION WAITS
as discussed in Blog TM lock and no transaction commit.

Wednesday, April 17, 2019

LOB ORA-22924: snapshot too old and Fix

To continue the discussion in Blog: UNDO Practice, this Blog will demonstrate LOB special ORA-01555 UNDO error, in which both rollback segment number and name are null:
    ORA-01555: snapshot too old: rollback segment number  with name "" too small
    ORA-22924: snapshot too old

    (ORA-01555 printf format string is:
      01555, 00000, "snapshot too old: rollback segment number %s with name \"%s\" too small")
We will test such "too old" in two dimensions, one is according to space usage (LOB pctversion), other is according to life time (LOB retention).

The code examples are in Plsql. After the test, we also try to provide one fix.

At beginning, it was thought to find some concrete code examples to reproduce ORA-22924. Googled with "Oracle LOB ORA-22924: snapshot too old example", and paged over a dozen of returned results, it was still empty.

Note: All tests are done in 12.1.0.2.0 (12cR1)


1. Test Setup


First we create a table containing one LOB column and fill some data. The LOB column is stored as basicfile and using pctversion to control the old versions of LOB data. The pctversion is set to special value 0 so that ORA-22924 can be reproduced in each short run. We also tested pctversion default value 10, the same ORA-22924 is still reproducible (later we will also test retention Parameter).

---==================== PCTVERSION Test Setup ====================---

drop tablespace test_ts including contents and datafiles;

create tablespace test_ts datafile '/oradb/oradata/testdb/test_dbf.dbf' size 100m online;

drop table tab_lob cascade constraints;

create table tab_lob(id number, mylob clob) 
  lob (mylob) store as basicfile 
  (tablespace  test_ts
   enable      storage in row
   chunk       8192
   pctversion 0
   --pctversion 10        -- default of 10 (%)
   --retention   none
   nocache
   logging)
tablespace test_ts; 

declare
  l_cnt     number := 1e1;
  l_clob    clob   := to_clob(rpad('abc', 10000, 'x'));
begin
  for i in 1..l_cnt loop
    insert into tab_lob values(i, l_clob);
  end loop;
  commit;
end;
/   
Show LOB meta info:

---==================== PCTVERSION Test Meta Info ====================---
 
column table_name format a14;
column column_name format a14;
column segment_name format a28;
column column_name format a14;
column retention_type format a20;
  
select table_name, column_name, segment_name, pctversion, retention, retention_type 
  from dba_lobs where table_name in ('TAB_LOB');  
  
TABLE_NAME COLUMN_NAME SEGMENT_NAME              PCTVERSION  RETENTION RETENTION_TYPE
---------- ----------- ------------------------- ---------- ---------- --------------
TAB_LOB    MYLOB       SYS_LOB0003449207C00002$$          0            NO
  
 
select segment_name, segment_type from dba_segments where tablespace_name in ('TEST_TS');

SEGMENT_NAME                 SEGMENT_TYPE
---------------------------- ------------
TAB_LOB                      TABLE
SYS_IL0003449207C00002$$     LOBINDEX
SYS_LOB0003449207C00002$$    LOBSEGMENT


-- The names of LOB object and index are composed by table OBJECT_ID (3449207) 
-- with prefix "SYS_LOB"/"SYS_IB" and suffix "C00002$$".

select object_name, object_id, object_type from dba_objects 
 where object_name in ('TAB_LOB', 'SYS_LOB0003449207C00002$$', 'SYS_IL0003449207C00002$$')
 order by object_id;

OBJECT_NAME                OBJECT_ID   OBJECT_TYPE
------------------------- ----------   -----------
TAB_LOB                      3449207   TABLE
SYS_LOB0003449207C00002$$    3449208   LOB
SYS_IL0003449207C00002$$     3449209   INDEX
Create 3 procedures for our test.

---==================== PCTVERSION Test Meta Info ====================---

create or replace procedure lob_22924_select(p_id number, p_cnt number, p_sleep number) as
  l_clob           clob;
  l_null_check     boolean;
begin
  select mylob into l_clob from tab_lob where id = p_id;
  
  for i in 1..p_cnt loop
   dbms_output.put_line('------- Seq: '||i);
   
   -- getlength, no error
    dbms_output.put_line('LOB length check OK, length = '||dbms_lob.getlength(l_clob));
    
    -- null check, no error
    l_null_check := l_clob is null;
    l_null_check := l_clob is not null;
    dbms_output.put_line('LOB null check OK');
    
    -- content access, throw ORA-22924 under ORA-01555
    dbms_output.put_line('LOB content check, substr ='||dbms_lob.substr(l_clob, 10, 2000));
    
    dbms_lock.sleep(p_sleep);
  end loop;
end;
/

create or replace procedure lob_22924_update(p_id number, p_cnt number, p_sleep number) as
  l_clob      clob;
  l_pad       varchar2(1000) := rpad('abc', 100, 'x');
begin
  select mylob into l_clob from tab_lob where id = p_id;
  
  for i in 1..p_cnt loop
   update tab_lob set mylob = mylob||l_pad where id = p_id;
    commit;
    dbms_lock.sleep(p_sleep);
  end loop;
end;
/

create or replace procedure lob_22924_select_update(p_id number, p_cnt number, p_sleep number) as
  l_clob        clob;
  l_pad         varchar2(1000) := rpad('abc', 100, 'x');
  l_clob_upd    clob := to_clob(rpad('abc', 10000, 'x'));  
  l_null_check  boolean;
begin
  select mylob into l_clob from tab_lob where id = p_id;
  
  for i in 1..p_cnt loop
    dbms_output.put_line('------- Seq: '||i);  
    update tab_lob set mylob = mylob||l_pad where id = p_id;
    commit;
    
    -- getlength, no error
    dbms_output.put_line('LOB length check OK, length = '||dbms_lob.getlength(l_clob));
    
    -- null check, no error
    l_null_check := l_clob is null;
    l_null_check := l_clob is not null;
    dbms_output.put_line('LOB null check OK');
    
    -- content access, throw ORA-22924 under ORA-01555
    dbms_output.put_line('LOB content check, substr ='||dbms_lob.substr(l_clob, 10, 2000));
    
    dbms_lock.sleep(p_sleep);
  end loop;
end;
/


2. Test Run


We will make two different tests to generate ORA-22924. Once with two Sqlplus sessions, once with a single session.


2.1. Two Sessions.


We open two Sqlplus Sessions. In Session_1, call lob_22924_select to start a query:

-------------- Session_1@T1 select --------------
 
10:22:15 Sql > exec lob_22924_select(3, 1e2, 1);
  ------- Seq: 1
  LOB length check OK, length = 10000
  LOB null check OK
  LOB content check, substr =xxxxxxxxxx
  ------- Seq: 2
  LOB length check OK, length = 10000
  LOB null check OK
  LOB content check, substr =xxxxxxxxxx
  
  ...
  ------- Seq: 7
  LOB length check OK, length = 10000
  LOB null check OK
  BEGIN lob_22924_select(3, 1e2, 1); END;
  
  *
  ERROR at line 1:
  ORA-01555: snapshot too old: rollback segment number  with name "" too small
  ORA-22924: snapshot too old
  ORA-06512: at "SYS.DBMS_LOB", line 1109
  ORA-06512: at "S.LOB_22924_SELECT", line 19
  ORA-06512: at line 1
In Session_2, call lob_22924_update to start a loop update.

-------------- Session_2@T2 update --------------

10:22:24 Sql > exec lob_22924_update(3, 1e4, 0.01);
After a couple of seconds, Session_1 throws error ORA-01555 and ORA-22924. If we look procedure lob_22924_select, error occurs when we access content by dbms_lob.substr. For dbms_lob.getlength and LOB null check, there is no such error. Probably both dbms_lob.getlength and LOB null check are using LOB index, and LOB index is based on normal Oracle UNDO mechanism.

Session_1 output shows that LOB length is always 10000, which implies that the checked LOB data is pointing to the fetched data and never changed. It acts like a consistent view in READ ONLY transaction (or SERIALIZABLE transaction).


2.2. One Session


We open one single Sqlplus Session Session_2, call lob_22924_select_update to start a query, then make updates:

-------------- Session_3@T3 update --------------

10:28:33 Sql > exec lob_22924_select_update(7, 1e2, 1);
  ------- Seq: 1
  LOB length check OK, length = 10000
  LOB null check OK
  LOB content check, substr =xxxxxxxxxx
  ------- Seq: 2
  LOB length check OK, length = 10000
  LOB null check OK
  LOB content check, substr =xxxxxxxxxx
  
  ...
  ------- Seq: 54
  LOB length check OK, length = 10000
  LOB null check OK
  BEGIN lob_22924_select_update(7, 1e2, 1); END;
  
  *
  ERROR at line 1:
  ORA-01555: snapshot too old: rollback segment number  with name "" too small
  ORA-22924: snapshot too old
  ORA-06512: at "SYS.DBMS_LOB", line 1109
  ORA-06512: at "S.LOB_22924_SELECT_UPDATE", line 23
  ORA-06512: at line 1
After about 50 seconds, it throws error ORA-01555 and ORA-22924.

lob_22924_select_update is a merge of previous select (lob_22924_select) and update (lob_22924_update). When we run both in one single session, we get the same error. That means even though we update the current LOB version, the old consistent version is still kept in the same session. And any access to the content can hit ORA-22924 error.


3. Retention Test


LOB column can also be configured to store old versions of LOB data for a period of time by normal retention, rather than using a percentage of the table space by above pctversion.

First we change the DDL to use retention, fill data and show meta info:

---==================== RETENTION Test ====================

alter system set undo_retention = 900;    --(Default 900)

drop table tab_lob cascade constraints;

create table tab_lob(id number, mylob clob) 
  lob (mylob) store as basicfile 
  (tablespace  test_ts
   enable      storage in row
   chunk       8192
   --pctversion 0
   retention   none
   nocache
   logging)
tablespace test_ts; 

declare
  l_cnt     number := 1e1;
 l_clob    clob   := to_clob(rpad('abc', 10000, 'x'));
begin
  for i in 1..l_cnt loop
    insert into tab_lob values(i, l_clob);
  end loop;
  commit;
end;
/   

select table_name, column_name, segment_name, pctversion, retention, retention_type 
  from dba_lobs where table_name in ('TAB_LOB');  
  
TABLE_NAME COLUMN_NAME SEGMENT_NAME              PCTVERSION  RETENTION RETENTION_TYPE
---------- ----------- ------------------------- ---------- ---------- --------------
TAB_LOB    MYLOB       SYS_LOB0003449216C00002$$                   900 YES
We can see that default retention is picked from undo_retention default 900 seconds. To speed up out test, we can low it to a short time, for example, 3 seconds:

alter system set undo_retention = 3;  

alter table tab_lob modify lob (mylob) (retention);  
  
select table_name, column_name, segment_name, pctversion, retention, retention_type 
  from dba_lobs where table_name in ('TAB_LOB');  
  
TABLE_NAME COLUMN_NAME SEGMENT_NAME              PCTVERSION  RETENTION RETENTION_TYPE
---------- ----------- ------------------------- ---------- ---------- --------------
TAB_LOB    MYLOB       SYS_LOB0003449216C00002$$                     3 YES
Then restore original undo_retention, and recompile invalidated procedures:

alter system set undo_retention = 900; 

alter procedure lob_22924_select compile;
alter procedure lob_22924_select_update compile;
alter procedure lob_22924_update compile;    
Now we can repeat the same tests (two or one sessions) as pctversion, and get the same errors.

With retention, it can require longer time to erase the kept CR copy because of Oracle AUM (Automatic Undo Management). Therefore it needs to a higher update loops (p_cnt) when calling lob_22924_select_update to hit ORA-22924.

We also noticed that when running several concurrent update sessions (each updates a different row), the error appears quicker and more frequent because all LOB data (from different rows) are stored in the same datafile.


4. Fix


The problem of ORA-22924 is that we are retaining a LOB Locator to an old version of LOB data, and if this old version gets too old (overwritten by newer versions), we hit the error when accessing that Locator.

Oracle permanent LOB CR views are implemented by versions (different copies) to conform to the ANSI standard (ACID regime). LOB data does not generate rollback information (redo/undo). Only LOB Index generates undo/redo because it is implemented in normal Oracle undo/redo mechanism.

However for Oracle temporary LOBs, CR, undo and versions are not supported. They are stored in Temporary Tablespace and are session private.

In the fix below, LOB content is at first copied to a local temporary LOB so that we always preserve a CR data for later access (analogue to normal CR view at the point of query start).

Note that dbms_lob.copy is used to create a new CR copy (new instance) of LOB content. Direct LOB assignment by Sql or Plsql do not fix ORA-22924: snapshot too old.

create or replace procedure lob_22924_select_fix(p_id number, p_cnt number, p_sleep number) as
  l_clob           clob;
  l_null_check     boolean;
  l_clob_temp      clob;
begin
  -- create temporary lob
  dbms_lob.createtemporary(lob_loc => l_clob_temp, cache => true, dur => dbms_lob.call);
  
  select mylob into l_clob from tab_lob where id = p_id;
  
  -- copy content of Permanent LOB Locator to Temporary LOB Locator (pass by value)
  dbms_lob.copy(dest_lob => l_clob_temp, src_lob => l_clob, amount => dbms_lob.getlength(l_clob));
  
  -- Note: two following approaches not fix ORA-22924: snapshot too old 
  -- because Temporary LOB Locator is overwritten by Permanent LOB Locator
  -- and Temporary LOB Locator points to the Permanent LOB Locator (pass by pointer). 
  --   select mylob into l_clob_temp from tab_lob where id = p_id;   -- not fix ORA-22924 by Sql
  --   l_clob_temp := l_clob;                                        -- not fix ORA-22924 by Plsql
  
  for i in 1..p_cnt loop
    dbms_output.put_line('------- Seq: '||i);
   
    -- getlength, no error
    dbms_output.put_line('LOB length check OK, length = '||dbms_lob.getlength(l_clob_temp));
    
    -- null check, no error
    l_null_check := l_clob_temp is null;
    l_null_check := l_clob_temp is not null;
    dbms_output.put_line('LOB null check OK');
    
    -- content access by local temp copy, not throw ORA-22924 under ORA-01555
    dbms_output.put_line('LOB content check, substr ='||dbms_lob.substr(l_clob_temp, 10, 2000));
    
    dbms_lock.sleep(p_sleep);
  end loop;
  
  -- free temporary lob
  dbms_lob.freetemporary(lob_loc => l_clob_temp);
end;
/ 

Wednesday, November 7, 2018

Row Cache Objects, Row Cache Latch on Object Type: Plsql vs Java Call (Part-2) (III)


(I)-Tracing Methods      (II)-Object Type(Part-1)      (III)-Object Type(Part-2)       (IV)-Sql Executions (IV)      (V)-Contentions and Scalability


Continue from Blog Part-1 (Row Cache Objects, Row Cache Latch on Object Type: Plsql vs Java Call (Part-1) (II) ), this Blog will explore more object-oriented features of Object Types, and demonstrate the impact on Row Cache Object GETs, and Row Cache Latch GETs.

Based on Part-1 Test Code, we define two super classes (see appended PLSQL Test Code):
  t_obj_ref_super for t_obj_ref
  t_obj_ret_super for t_obj_ret
Then we try different ways of using them in Plsql code and Java code, and monitor the consequence on Row Cache Object GETs.

In Section 9. Plsql DataType Test of this Blog, we added the test of Plsql DataType: record, nested table, associative Array in both Plsql dynamic call and Java call (with JDBC Connection Pool).

Note: All tests are done in Oracle 12.1.0.2.


1. Code Changes


Compare to Part-1, following changes are made in In this Part-2 Blog (see appended Test Code).
  -. Setup a super Class: T_OBJ_RET_SUPER, and put T_OBJ_RET under it.
  -. Introduce a new Object Type: T_OBJ_REF, and its super Class: T_OBJ_REF_SUPER.
  -. Create a new procedure test_ref_call using above Object Types.
  -. Add a new line in Part-1 function foo to invoke test_ref_call. 
Here the new procedure test_ref_call (V1), and the modified function foo.

---=========== V1, t_obj_ret, callforward with 0 t_obj_ref CID call, 2 t_obj_ret CID call  ==========---
create or replace procedure test_ref_call(p_obj_ret in out nocopy t_obj_ret, p_id number) 
as
  l_obj_ref      t_obj_ref;
begin
  l_obj_ref      := new t_obj_ref(p_id);
  p_obj_ret.callforward(l_obj_ref, p_id, 'TEST_REF_PROC_1.name');
end;
/

---=====================---
create or replace function foo (
  p_in    in     t_obj_in
 ,p_out      out t_obj_out
 ,p_inout in out t_obj_inout) return t_obj_ret 
as
  l_ret          t_obj_ret;
begin
  -- l_ret.id return 1122+112=1234
  l_ret      := t_obj_ret(p_in.id + 112, p_in.name);
  p_out      := t_obj_out(p_inout.id, p_inout.name);
  p_inout.id := l_ret.id; 
  
  -- Subroutine using T_OBJ_REF added in Part-2
  test_ref_call(l_ret, p_in.id);
   
 return l_ret;
end;
/


2. Case_1: No Super Class Call


In Case_1, test_ref_call calls t_obj_ret.callforward, without using any super classes.

---=========== V1, t_obj_ret, callforward with 0 t_obj_ref CID call, 2 t_obj_ret CID call  ==========---
create or replace procedure test_ref_call(p_obj_ret in out nocopy t_obj_ret, p_id number) 
as
  l_obj_ref      t_obj_ref;
begin
  l_obj_ref      := new t_obj_ref(p_id);
  p_obj_ret.callforward(l_obj_ref, p_id, 'TEST_REF_PROC_1.name');
end;
/
Test below shows that Row Cache Object GETs is same as function foo in Part-1 in both Plsql and Java . There are no t_obj_ref Row Cache Object GETs.


2.1. Plsql Dynamic Call


Same as Part-1, Call foo 100 times:

BEGIN :1 := foo(:2, :3, :4); END;
by foo_proc with execute immediate, and monitor Row Cache Objects Gets with Dtrace Script: rco_dtrace_cache (see Blog: Oracle row cache objects Event: 10222, Dtrace Script (I)):

begin
  trc_start(4294967295);
  foo_proc(100);
  trc_stop;
end;
/

*************** CID Stats with Address ****************
CID = 17   ADDR = 17124A3B0        CNT = 503
CID = 11   ADDR = 16F100318        CNT = 502
CID = 7    ADDR = 1717B2250        CNT = 497

*************** CID Stats ****************
CID = 17   CNT = 509
CID = 11   CNT = 509  --1 T_OBJ_RET, 1 T_OBJ_IN, 1 T_OBJ_OUT, 2 T_OBJ_INOUT
CID = 7    CNT = 530

*************** CID Stats Summary ****************
CNT = 1554


2.2. Java Call


Call foo 100 times in Java CallableStatement:

BEGIN :1 := K.FOO(:2, :3, :4); END;
with command:

java RCOObjTypeJDBC1 "jdbc:oracle:thin:k/s@testDB:1521:testDB" 100 1
and monitor Row Cache Objects Gets with Dtrace Script: rco_dtrace_cache.

*************** CID Stats with Address ****************
CID = 17   ADDR = 178D70438        CNT = 701
CID = 11   ADDR = 16F1C6A48        CNT = 696
CID = 7    ADDR = 172403178        CNT = 700

*************** CID Stats ****************
CID = 17   CNT = 708
CID = 11   CNT = 704   -- 2 T_OBJ_RET, 1 T_OBJ_IN, 2 T_OBJ_OUT, 2 T_OBJ_INOUT
CID = 7    CNT = 715

*************** CID Stats Summary ****************
CNT = 2160


3. Case_2: using Super Class: t_obj_ref_super


Redefine test_ref_call (V2) to call t_obj_ret.callforward_super, instead of callforward. The difference is callforward_super's first parameter is super class of t_obj_ref.
  callforward      (p_obj_ref in out nocopy t_obj_ref,       p_id number, p_name varchar2)
  callforward_super(p_obj_ref in out nocopy t_obj_ref_super, p_id number, p_name varchar2)
Now we can see there is 1 additional T_OBJ_REF Row Cache Object Get in comparing to above foo call in both Plsql and Java.

---=========== V2, using Super Class: t_obj_ref_super: 1 t_obj_ref CID call, 2 t_obj_ret CID call ==========---
create or replace procedure test_ref_call(p_obj_ret in out nocopy t_obj_ret, p_id number) 
as
  l_obj_ref      t_obj_ref;
begin
  l_obj_ref      := new t_obj_ref(p_id);
  p_obj_ret.callforward_super(l_obj_ref, p_id, 'TEST_REF_PROC_1.name');
end;
/


3.1 Plsql Dynamic Call


---=========== V2, test_ref_call ==========---
*************** CID Stats with Address ****************
CID = 17   ADDR = 172298F88        CNT = 100  --T_OBJ_RET
CID = 17   ADDR = 17124A5E0        CNT = 100  --T_OBJ_IN
CID = 17   ADDR = 17124A810        CNT = 100  --T_OBJ_OUT
CID = 17   ADDR = 17179CB18        CNT = 101  --T_OBJ_REF
CID = 17   ADDR = 17124A3B0        CNT = 200  --T_OBJ_INOUT

CID = 11   ADDR = 16DA41CD8        CNT = 100  --T_OBJ_RET
CID = 11   ADDR = 16DA41478        CNT = 100  --T_OBJ_IN
CID = 11   ADDR = 16DA418A8        CNT = 100  --T_OBJ_OUT
CID = 11   ADDR = 16F367C50        CNT = 101  --T_OBJ_REF
CID = 11   ADDR = 16DA41048        CNT = 200  --T_OBJ_INOUT

CID = 7    ADDR = 1716FDFB0        CNT = 606

*************** CID Stats ****************
CID = 17   CNT = 616
CID = 11   CNT = 616    -- 1 T_OBJ_RET, 1 T_OBJ_IN, 1 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF
CID = 7    CNT = 633

*************** CID Stats Summary ****************
CNT = 1877


3.2. Java Call


--------------- Java Level 4294967295 --------------
---=========== V2, test_ref_call ==========---
CID = 17   ADDR = 172298F88        CNT = 797
CID = 11   ADDR = 16F1A1B90        CNT = 797  
CID = 7    ADDR = 17169DA78        CNT = 798  

----------------------
CID = 17   CNT = 829
CID = 11   CNT = 813   -- 2 T_OBJ_RET, 1 T_OBJ_IN, 2 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF
CID = 7    CNT = 822

----------------------
CNT = 2542


4. Case_3: using Super Class: t_obj_ret_super


Take above test_ref_call, change its first paramter to super class:
  t_obj_ret_super 
as the first paramter, instead of t_obj_ret.

Test shows that there is one more T_OBJ_RET Row Cache Object Get in comparing to above foo call in both Plsql and Java.

---=========== V3, using Super Class: t_obj_ret_super: 1 t_obj_ref CID call, 3 t_obj_ret CID call ==========---
create or replace procedure test_ref_call(p_obj_ret in out nocopy t_obj_ret_super, p_id number) 
as
  l_obj_ref      t_obj_ref;
begin
  l_obj_ref      := new t_obj_ref(p_id);
  p_obj_ret.callforward_super(l_obj_ref, p_id, 'TEST_REF_PROC_1.name');
end;
/


4.1 Plsql Dynamic Call


---=========== V3, test_ref_call ==========---
*************** CID Stats with Address ****************
CID = 17   ADDR = 17179CB18        CNT = 699
CID = 11   ADDR = 16F100318        CNT = 697  
CID = 7    ADDR = 171705DB0        CNT = 695

*************** CID Stats ****************
CID = 17   CNT = 708
CID = 11   CNT = 708   -- 2 T_OBJ_RET, 1 T_OBJ_IN, 1 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF
CID = 7    CNT = 718

*************** CID Stats Summary ****************
CNT = 2140


4.2. Java Call


---=========== V3, test_ref_call ==========---
*************** CID Stats with Address ****************
CID = 17   ADDR = 17C2A5200        CNT = 909
CID = 11   ADDR = 16F1A1B90        CNT = 896  
CID = 7    ADDR = 171705DB0        CNT = 911   

*************** CID Stats ****************
CID = 17   CNT = 930
CID = 11   CNT = 914  -- 3 T_OBJ_RET, 1 T_OBJ_IN, 2 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF
CID = 7    CNT = 923

*************** CID Stats Summary ****************
CNT = 2845


5. Case_4: Java register super Class as OutParameter


Now we can try to use super class in Java Class. In Java Test Code (see Blog Part-1) , replace line:

  cStmt.registerOutParameter(1, OracleTypes.STRUCT, "K.T_OBJ_RET");
by

  cStmt.registerOutParameter(1, OracleTypes.STRUCT, "K.T_OBJ_RET_SUPER");   
Run above Java test, 10222 trace file shows that
  1 T_OBJ_RET CID call
  2 T_OBJ_RET_SUPER CID calls
instead of
  3 T_OBJ_RET CID calls
in the previous test.


6. Case_5: Call by NULL value


If invoking the function by NULL value in Plsql Dynamic or Java, the cid GETs are not changed.
See Test Code (out-commented) in Blog: Row Cache Objects, Row Cache Latch on Object Type: Plsql vs Java Call (Part-1) (II)


7. Summary


The number of GETs is increased with the number of Super Classes inteventions.
Case_1: No Super Class Call: same as Part-1
  Plsql Dynamic Call: 
    1 T_OBJ_RET, 1 T_OBJ_IN, 1 T_OBJ_OUT, 2 T_OBJ_INOUT
        5*3 = 15 Row Cache Objects GETs, 15*3=45 Row Cache Latch GETs.
  Java Call:  
    2 T_OBJ_RET, 1 T_OBJ_IN, 2 T_OBJ_OUT, 2 T_OBJ_INOUT     
        7*3 = 21 Row Cache Objects GETs, 21*3=63 Row Cache Latch GETs.
  
Case_2: using Super Class: t_obj_ref_super: 1 additional T_OBJ_REF 
  Plsql Dynamic Call: 
    1 T_OBJ_RET, 1 T_OBJ_IN, 1 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF
        6*3 = 18 Row Cache Objects GETs, 18*3=54 Row Cache Latch GETs.  
  Java Call        :
    2 T_OBJ_RET, 1 T_OBJ_IN, 2 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF
        8*3 = 24 Row Cache Objects GETs, 24*3=72 Row Cache Latch GETs.  
  
Case_3: using Super Class: t_obj_ret_super: 1 additional T_OBJ_RET
  Plsql Dynamic Call: 
    2 T_OBJ_RET, 1 T_OBJ_IN, 1 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF
        7*3 = 21 Row Cache Objects GETs, 21*3=63 Row Cache Latch GETs. 
  Java Call        : 
    3 T_OBJ_RET, 1 T_OBJ_IN, 2 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF
        9*3 = 27 Row Cache Objects GETs, 27*3=81 Row Cache Latch GETs. 
  
Case_4: Java register super Class as OutParameter: 1 T_OBJ_RET, 2 T_OBJ_RET_SUPER
  Java Call:
    1 T_OBJ_RET, 2 T_OBJ_RET_SUPER, 1 T_OBJ_IN, 2 T_OBJ_OUT, 2 T_OBJ_INOUT, 1 T_OBJ_REF  
        9*3 = 27 Row Cache Objects GETs, 27*3=81 Row Cache Latch GETs. 

Case_5: Call by NULL value
  Plsql Dynamic Call:  no influence, same cid GETs
  Java Call:           no influence, same cid GETs
More complexity can be further investigated, for example, Object Collections (Associative array, Nested table, Varray), attribute Object Types.

In Section 9. Plsql DataType Test of this Blog, we also made Plsql DataType Test in Plsql dynamic call and Java call (with JDBC Connection Pool).


8. Plsql Test Code


------------------------- parameters(IN, OUT, IN OUT) different Types -------------------

create or replace type t_obj_in    force as object (id number, name varchar2(30));
/

create or replace type t_obj_out   force as object (id number, name varchar2(30));
/

create or replace type t_obj_inout force as object (id number, name varchar2(30));
/

------------------------- t_obj_ref_super, t_obj_ref -------------------

create or replace type t_obj_ref_super force is object (dummy number
  ,member procedure callbackward(p_id number, p_name varchar2)
)
not final
not instantiable
/

create or replace type t_obj_ref force under t_obj_ref_super (id number, name varchar2(30)
  ,constructor function t_obj_ref (p_id number) return self as result
  ,overriding member procedure callbackward(p_id number, p_name varchar2)
);
/

create or replace type body t_obj_ref as
  constructor function t_obj_ref (p_id number) return self as result is
  begin
    id   := p_id;
    name := 'T_OBJ_REF.name';
    return;
  end;
  
  overriding member procedure callbackward(p_id number, p_name varchar2) as
    l_dummy varchar2(100);
    begin 
      id   := p_id;
      name := p_name;
      l_dummy := 'callbackward: '||p_id||'_'||p_name;
      debug(l_dummy);
    end; 
end;
/

------------------------- t_obj_ret_super, t_obj_ret -------------------

create or replace type t_obj_ret_super force as object (dummy number
  ,member procedure callforward      (p_obj_ref in out nocopy t_obj_ref,       p_id number, p_name varchar2)
  ,member procedure callforward_super(p_obj_ref in out nocopy t_obj_ref_super, p_id number, p_name varchar2))
not final
not instantiable
/

create or replace type t_obj_ret force under t_obj_ret_super (id number, name varchar2(30)
 ,constructor function t_obj_ret (p_id number, p_name varchar2) return self as result
 ,overriding member procedure callforward      (p_obj_ref in out nocopy t_obj_ref,       p_id number, p_name varchar2)
  ,overriding member procedure callforward_super(p_obj_ref in out nocopy t_obj_ref_super, p_id number, p_name varchar2))
/

create or replace type body t_obj_ret as
  constructor function t_obj_ret (p_id number, p_name varchar2) return self as result is
  begin
    id   := p_id;
    name := p_name;
    return;
  end;

  overriding member procedure callforward(p_obj_ref in out nocopy t_obj_ref, p_id number, p_name varchar2) as
    begin 
      p_obj_ref.callbackward(p_id, p_name);
    end; 
      
  overriding member procedure callforward_super(p_obj_ref in out nocopy t_obj_ref_super, p_id number, p_name varchar2) as
    begin 
      p_obj_ref.callbackward(p_id, p_name);
    end;  
end;
/

-------------------------------------------------------------------
---=== V1, t_obj_ret, callforward with 0 t_obj_ref CID call, 2 t_obj_ret CID call in Java ===---
create or replace procedure test_ref_call(p_obj_ret in out nocopy t_obj_ret, p_id number) 
as
  l_obj_ref      t_obj_ref;
begin
  l_obj_ref      := new t_obj_ref(p_id);
  p_obj_ret.callforward(l_obj_ref, p_id, 'TEST_REF_PROC_1.name');
end;
/

---=== V2, using Super Class: t_obj_ref_super: 1 t_obj_ref CID call, 2 t_obj_ret CID call in Java ===---
create or replace procedure test_ref_call(p_obj_ret in out nocopy t_obj_ret, p_id number) 
as
  l_obj_ref      t_obj_ref;
begin
  l_obj_ref      := new t_obj_ref(p_id);
  p_obj_ret.callforward_super(l_obj_ref, p_id, 'TEST_REF_PROC_1.name');
end;
/

---=== V3, using Super Class: t_obj_ret_super: 1 t_obj_ref CID call, 3 t_obj_ret CID call in Java ===---
create or replace procedure test_ref_call(p_obj_ret in out nocopy t_obj_ret_super, p_id number) 
as
  l_obj_ref      t_obj_ref;
begin
  l_obj_ref      := new t_obj_ref(p_id);
  p_obj_ret.callforward_super(l_obj_ref, p_id, 'TEST_REF_PROC_1.name');
end;
/

---=====================---
create or replace function foo (
  p_in    in     t_obj_in
 ,p_out      out t_obj_out
 ,p_inout in out t_obj_inout) return t_obj_ret 
as
  l_ret          t_obj_ret;
begin
  -- l_ret.id return 1122+112=1234
  l_ret      := t_obj_ret(p_in.id + 112, p_in.name);
  p_out      := t_obj_out(p_inout.id, p_inout.name);
  p_inout.id := l_ret.id; 
  
  -- T_OBJ_REF call line added in Part-2
  test_ref_call(l_ret, p_in.id);
   
  return l_ret;
end;
/


9. Plsql DataType Test


We tested Plsql DataType: record, nested table, associative Array in Plsql dynamic call and Java call. Here the test result and test code.

------ Plsql DataType Test Result: Plsql dynamic vs. Java ------

1. Plsql DataType: record
     Plsql dynmic Call: no CID increase
     Java Call:         each Call, 2 Calls of ('dc_global_oids' on "TEST_JDBC_PKG'", 'dc_objects' on "TEST_JDBC_PKG", 'dc_users' on "K")

2. Plsql DataType: nested table (@TODO varray)
     Plsql dynmic Call: no CID increase
     Java Call:         each Call, 2 Calls of ('dc_global_oids' on "TEST_JDBC_PKG'", 'dc_objects' on "TEST_JDBC_PKG", 'dc_users' on "K") 

3. Plsql DataType: associative Array - scalar type
     Plsql dynmic Call: no CID increase
     Java Call:         no CID increase     

4. Plsql DataType: associative Array - record type
     Plsql dynmic Call: no CID increase
     Java Call:         NOT supported. Only support basic scalar types (NUMERIC and VARCHAR)
       See: Database JDBC Developer's Guide and Reference, Accessing PL/SQL Index-by Tables 
           (https://docs.oracle.com/cd/B28359_01/java.111/b31224/oraint.htm#BABBGDFA)       

---------------- Plsql DataType SetUp -------------------

create or replace package k.TEST_JDBC_PKG is
  type PLSQL_RECORD is record(name varchar2(100), id pls_integer);
  
  type PLSQL_TAB is table of number;
  
  type PLSQL_AARRAY is table of varchar2(10) index by pls_integer;
  
  type PLSQL_RECARY is table of PLSQL_RECORD index by pls_integer;
end;
/

-- Plsql dynmic Call: no CID increase
-- Java Call:         each Call, 2 x ('dc_global_oids' on "TEST_JDBC_PKG'", 'dc_objects' on "TEST_JDBC_PKG", 'dc_users' on "K")
create or replace procedure foo_proc_record (o_rc out nocopy TEST_JDBC_PKG.PLSQL_RECORD) as
begin
  o_rc.name := 'ksun'; 
 o_rc.id   := 1122;
end;
/

-- Plsql dynmic Call: no CID increase
-- Java Call:         each Call, 1 x ('dc_global_oids' on "TEST_JDBC_PKG'", 'dc_objects' on "TEST_JDBC_PKG", 'dc_users' on "K") 
create or replace procedure foo_proc_tab (o_tab out nocopy TEST_JDBC_PKG.PLSQL_TAB) as
begin
  --initialization mandatory; otherwise, ORA-06531: Reference to uninitialized collection
  o_tab := new TEST_JDBC_PKG.PLSQL_TAB();   
  o_tab.extend(2);
  o_tab(1) := 11; 
 o_tab(2) := 22;
end;
/

-- Plsql dynmic Call: no CID increase
-- Java Call:         no CID increase
create or replace procedure foo_proc_aarray (o_ary out nocopy TEST_JDBC_PKG.PLSQL_AARRAY) as
begin
  o_ary(1) := 'aaa'; 
 o_ary(2) := 'bbb';
end;
/

-- Plsql dynmic Call: no CID increase
-- Java Call:         NOT supported. Only support basic scalar types (NUMERIC and VARCHAR)
--   See: Database JDBC Developer's Guide and Reference, Accessing PL/SQL Index-by Tables 
--      (https://docs.oracle.com/cd/B28359_01/java.111/b31224/oraint.htm#BABBGDFA)
create or replace procedure foo_proc_recary (o_ray out nocopy TEST_JDBC_PKG.PLSQL_RECARY) as
begin
  o_ray(1).name := 'aaaa';
 o_ray(1).id   := 111; 
 o_ray(2).name := 'bbbb';
 o_ray(2).id   := 222;
end;
/

---------------- Plsql Test Code -------------------

create or replace procedure foo_proc_record_loop (p_cnt number) as
  l_stmt            varchar2(100);
  l_record          TEST_JDBC_PKG.PLSQL_RECORD;
begin
  l_stmt := q'[begin K.foo_proc_record(:1); end;]';
  
  for i in 1..p_cnt loop
    execute immediate l_stmt using OUT l_record;
  end loop;
  dbms_output.put_line('l_record.name=' ||l_record.name);
end;
/

--exec foo_proc_record_loop(10);

create or replace procedure foo_proc_tab_loop (p_cnt number) as
  l_stmt          varchar2(100);
  l_tab           TEST_JDBC_PKG.PLSQL_TAB := new TEST_JDBC_PKG.PLSQL_TAB();
begin
  l_stmt := q'[begin K.foo_proc_tab(:1); end;]';
  
  --l_tab.extend(2);
  for i in 1..p_cnt loop
    execute immediate l_stmt using OUT l_tab;
  end loop;
  dbms_output.put_line('l_tab.count=' ||l_tab.count);
  dbms_output.put_line('l_tab(2)=' ||l_tab(2));
end;
/

--exec foo_proc_tab_loop(10);

create or replace procedure foo_proc_aarray_loop (p_cnt number) as
  l_stmt          varchar2(100);
  l_ary           TEST_JDBC_PKG.PLSQL_AARRAY;
begin
  l_stmt := q'[begin K.foo_proc_aarray(:1); end;]';
  
  --l_tab.extend(2);
  for i in 1..p_cnt loop
    execute immediate l_stmt using OUT l_ary;
  end loop;
  dbms_output.put_line('l_ary.count=' ||l_ary.count);
  dbms_output.put_line('l_ary(2)=' ||l_ary(2));
end;
/

--exec foo_proc_aarray_loop(10);

create or replace procedure foo_proc_recary_loop (p_cnt number) as
  l_stmt          varchar2(100);
  l_ray           TEST_JDBC_PKG.PLSQL_RECARY;
begin
  l_stmt := q'[begin K.foo_proc_recary(:1); end;]';
  
  --l_tab.extend(2);
  for i in 1..p_cnt loop
    execute immediate l_stmt using OUT l_ray;
  end loop;
  dbms_output.put_line('l_ray.count=' ||l_ray.count);
  dbms_output.put_line('l_ray(2).id=' ||l_ray(2).id);
end;
/

--exec foo_proc_recary_loop(10);

//------------ Java Test Code with Connection Pool ------------//

/**
Tested with OracleDataSource Connection Pool, can also tested with PoolDataSourceFactory (UCP).

-- setup CLASSPATH
  export CLASSPATH=$CLASSPATH:java-path:$ORACLE_HOME/jdbc/lib/ojdbc8.jar:$ORACLE_HOME/ucp/lib/ucp.jar:ODSTestDBOraclePool-path

-- Compile
 /usr/java/bin/javac ODSJDBCUtils.java ODSTestDBOraclePoolThread.java ODSTestDBOraclePool.java

-- Run
 /usr/java/bin/java ODSTestDBOraclePool    

-- Example
 /usr/java/bin/java -Xmx30m -Xms8m ODSTestDBOraclePool 3 100 1000 2

-- useCase
   1. SQL Object Type Test, "BEGIN :1 := K.FOO(:2, :3, :4); END;"
   2. Plsql DataType Record, "BEGIN K.FOO_PROC_RECORD(:1); END;"
   3. Plsql DataType Nested Table, "BEGIN K.FOO_PROC_TAB(:1); END;"
   4. Plsql DataType Associative Array, "BEGIN K.FOO_PROC_AARRAY(:1); END;"
   
-- Ref: Connect to Oracle using a connection pool (https://www.rgagnon.com/javadetails/java-0545.html)   
*/

//------------------------- ODSJDBCUtils.java -------------------------------//

import oracle.jdbc.pool.OracleDataSource;
import oracle.jdbc.pool.OracleConnectionCacheManager;

import java.util.Properties;
import java.sql.*;

public class ODSJDBCUtils {
    private final  static String CACHE_NAME = "KSUN_CACHE";
    private static OracleDataSource ods = null;
    static {
        System.out.println("OracleDataSource Initialization");
        try {
            ods = new OracleDataSource();
          
            ods.setURL("jdbc:oracle:thin:k/s@testdb:1521:testdb"); // thin Use TNS listener
          //ods.setURL("jdbc:oracle:oci8:k/s@testdb:1521:testdb"); //Maybe for Accessing PL/SQL Index-by Tables           
          //ods.setURL("jdbc:oracle:thin:@");     // thin Using Bequeath Connection, no need of TNS:no listener. thin support Bequeath from JDBC 23c
          //{ods.setURL("jdbc:oracle:oci:@");}    // oci Use Bequeath Connection, no need of TNS listener
          //{ods.setURL("jdbc:oracle:oci:@testdb:1521:testdb");}  // oci Use TNS listener
          //ods.setURL("jdbc:oracle:oci8:k/s@testdb:1521:testdb"); //Maybe for Accessing PL/SQL Index-by Tables
          
          ods.setUser("k");
          ods.setPassword("k");                 
            // caching parms
            ods.setConnectionCachingEnabled(true);
            ods.setConnectionCacheName(CACHE_NAME);
            Properties cacheProps = new Properties();
            cacheProps.setProperty("MinLimit", "2");
            cacheProps.setProperty("MaxLimit", "60");
            cacheProps.setProperty("InitialLimit", "2");
            cacheProps.setProperty("ConnectionWaitTimeout", "5");
            cacheProps.setProperty("ValidateConnection", "true");
            ods.setConnectionCacheProperties(cacheProps);
        }
        catch (SQLException e) {
            e.printStackTrace();
        }
    }
        
    private ODSJDBCUtils() { }

    public static Connection getConnection() throws SQLException {
      return getConnection("env. unspecified");
    }

    public static Connection getConnection(String thrd)
       throws SQLException
    {
      System.out.println("Request connection for " + thrd);
      if (ods == null) {
          throw new SQLException("OracleDataSource is null.");
      }
      return ods.getConnection();
    }
    
    public static void closePooledConnections() throws SQLException{
      if (ods != null ) {
          ods.close();
      }
    }

    public static void listCacheInfos() throws SQLException{
      OracleConnectionCacheManager occm = OracleConnectionCacheManager.getConnectionCacheManagerInstance();
      System.out.println (occm.getNumberOfAvailableConnections(CACHE_NAME) + " connections are available in cache " + CACHE_NAME);
      System.out.println (occm.getNumberOfActiveConnections(CACHE_NAME)    + " connections are active");
    }
 }

//-------------------------- ODSTestDBOraclePoolThread.java ------------------------------//

import java.sql.*;
import java.util.*;
import oracle.jdbc.pool.*;
import oracle.jdbc.*;
import java.math.BigDecimal;
import oracle.sql.ARRAY;
import oracle.sql.ArrayDescriptor;
import oracle.sql.STRUCT;
import oracle.sql.StructDescriptor;
import oracle.sql.Datum;
import oracle.ucp.jdbc.PoolDataSourceFactory;
import oracle.ucp.jdbc.PoolDataSource;

public class ODSTestDBOraclePoolThread implements Runnable {

    private int noThread = 0;
    private int loopCNT  = 0;
    private int sleepMs  = 1000;
    private int useCase  = 2;
    static String TESTSTMT_OBJ    = "BEGIN :1 := K.FOO(:2, :3, :4); END;";
    static String TESTSTMT_RECORD = "BEGIN K.FOO_PROC_RECORD(:1); END;";
    static String TESTSTMT_TAB    = "BEGIN K.FOO_PROC_TAB(:1); END;";
    static String TESTSTMT_AARRAY = "BEGIN K.FOO_PROC_AARRAY(:1); END;";

    ODSTestDBOraclePoolThread(int n, int loopCount, int sleepMili, int useCaseDef) {
        noThread = n;
        loopCNT  = loopCount;
        sleepMs  = sleepMili;
        useCase  = useCaseDef;
    }

   static void objCall(Connection conn, int loopCNT) {
     CallableStatement cStmt;
 
     if (conn != null) {
       try {
         cStmt = conn.prepareCall(TESTSTMT_OBJ);
         StructDescriptor dpIn    = StructDescriptor.createDescriptor("K.T_OBJ_IN", conn);
         StructDescriptor dpInOut = StructDescriptor.createDescriptor("K.T_OBJ_INOUT", conn);
         System.out.println("Call dbcall");
         Object [] objField = new Object[2];
 
         objField[0] = new Integer(1122);
         objField[1] = new String("T_OBJ_IN.name");
 
         STRUCT objIn    = new STRUCT(dpIn, conn, objField);
         STRUCT objInOut = new STRUCT(dpInOut, conn, objField);
 
         STRUCT objRet = null;
         STRUCT objOut = null;
         BigDecimal idVal;
 
         for(int i=0; i < loopCNT; i++){
             cStmt.registerOutParameter(1, OracleTypes.STRUCT, "K.T_OBJ_RET");
             cStmt.setObject(2, objIn);
             cStmt.setObject(4, objInOut);
             cStmt.registerOutParameter(3, OracleTypes.STRUCT, "K.T_OBJ_OUT");
             cStmt.registerOutParameter(4, OracleTypes.STRUCT, "K.T_OBJ_INOUT");
             cStmt.execute();
             objRet   = (STRUCT)cStmt.getObject(1);
             objOut   = (STRUCT)cStmt.getObject(3);
             objInOut = (STRUCT)cStmt.getObject(4);
         }
         idVal = (BigDecimal)objOut.getAttributes()[0];
         System.out.println(TESTSTMT_OBJ + " *** objOut.id =" + idVal.intValue());
         cStmt.close();
       } catch (SQLException e) {e.printStackTrace();}
     } else {
       System.out.println("Failed to make connection!");
     }
     System.out.println("*** objCall ***");
   }
    
    static void recordCall(Connection conn, int loopCNT) {
     CallableStatement cStmt;
     STRUCT     aRecord = null;
     String     rName   = null;
     BigDecimal rId     = null;
     
     if (conn != null) {
        try {
           cStmt = conn.prepareCall(TESTSTMT_RECORD);
           for(int i=0; i < loopCNT; i++){
               cStmt.registerOutParameter(1, OracleTypes.STRUCT, "K.TEST_JDBC_PKG.PLSQL_RECORD");
               cStmt.execute();
               aRecord = (STRUCT)cStmt.getObject(1);
               rName = (String)aRecord.getAttributes()[0];
               rId   = (BigDecimal)aRecord.getAttributes()[1];
           }
           System.out.println(TESTSTMT_RECORD + " *** Name = " + rName + " ,ID = " + rId);
           cStmt.close();
       } catch (SQLException e) {e.printStackTrace();}
    } else {
        System.out.println("Failed to make connection!");
    }
    }      
    
    static void tabCall(Connection conn, int loopCNT) {
     CallableStatement cStmt;
     ARRAY        aVArray   = null;
     BigDecimal[] numArray  = null;
     BigDecimal   aNum      = null;
     
     if (conn != null) {
        try {
           cStmt = conn.prepareCall(TESTSTMT_TAB);
           for(int i=0; i < loopCNT; i++){
               cStmt.registerOutParameter(1, OracleTypes.ARRAY, "K.TEST_JDBC_PKG.PLSQL_TAB");
               cStmt.execute();
               aVArray  = (ARRAY)cStmt.getArray(1);
                  numArray = (BigDecimal[])aVArray.getArray();
               aNum     = (BigDecimal)numArray[0];
           }
           System.out.println(TESTSTMT_TAB + " *** numArray.length = " + numArray.length);
           System.out.println(TESTSTMT_TAB + " *** numArray[0] = " + (aNum==null? "NULL": aNum.intValue()));
           cStmt.close();
       } catch (SQLException e) {e.printStackTrace();}
    } else {
        System.out.println("Failed to make connection!");
    }
    }  
    
    static void aarrayCall(Connection conn, int loopCNT) {
     OracleCallableStatement cStmt;
     int maxAArrayLen  = 10;
      int elementMaxLen = 20;
      Datum[] valAArray = new Datum[0];
     
     if (conn != null) {
        try {
           cStmt =  (OracleCallableStatement)conn.prepareCall(TESTSTMT_AARRAY);
           for(int i=0; i < loopCNT; i++){
               cStmt.registerIndexTableOutParameter (1, maxAArrayLen, OracleTypes.VARCHAR, elementMaxLen);
               cStmt.execute();
               valAArray = cStmt.getOraclePlsqlIndexTable(1);
           }
           System.out.println(TESTSTMT_AARRAY + " *** valAArray.length = " + valAArray.length);
           for(int i = 0; i < valAArray.length; i++)
                  System.out.println (TESTSTMT_AARRAY + " *** valAArray[i] = " + valAArray[i].stringValue());
           cStmt.close();
       } catch (SQLException e) {e.printStackTrace();}
    } else {
        System.out.println("Failed to make connection!");
    }
    }      
    
    public void run() {
        System.out.println("Starting Thread " + noThread);
        while (true) {
            try {
                Connection conn = ODSJDBCUtils.getConnection("Thread: " + noThread);
                
                if      (useCase == 1) {objCall   (conn, loopCNT);}
                else if (useCase == 2) {recordCall(conn, loopCNT);}
                else if (useCase == 3) {tabCall   (conn, loopCNT);}
                else if (useCase == 4) {aarrayCall(conn, loopCNT);}
                else {System.out.println ("Give me your Test");}
                
                conn.setAutoCommit(false);
                Statement stmt = conn.createStatement();
                ResultSet rset =
                   stmt.executeQuery("select 'Run UseCase: " + useCase + "' from dual");
                while (rset.next())
                   System.out.println (rset.getString(1) + ", Thread: " + noThread);
                rset.close();
                stmt.close();
                ODSJDBCUtils.listCacheInfos();
                conn.close();
                System.out.println ("rset/stmt/conn.close OK");
            }
            catch (SQLException e) { e.printStackTrace();}
            finally {
                System.out.println ("Sleeping Thread: " + noThread);
                try {Thread.sleep(sleepMs);} catch(Exception e) { }
            }
        }
    }
}

//------------------------ ODSTestDBOraclePool.java --------------------------------//

import java.net.URL;
import java.sql.*;

public class ODSTestDBOraclePool {
    public static void main(String[] args) throws SQLException {
     int threadCNT   = Integer.parseInt(args[0]);
     int loopCount   = Integer.parseInt(args[1]);
     int sleepMili   = Integer.parseInt(args[2]);
     int useCase     = Integer.parseInt(args[3]);
     for(int i=1; i <= threadCNT; i++){
        new Thread( new ODSTestDBOraclePoolThread(i, loopCount, sleepMili, useCase)).start();
      }
    }
}

Latch: row cache objects Contentions and Scalability (V)


(I)-Tracing Methods      (II)-Object Type(Part-1)      (III)-Object Type(Part-2)       (IV)-Sql Executions (IV)      (V)-Contentions and Scalability


This Blog will test the scalability of row cache objects discussed in previous 4 Blogs:
  1. Oracle row cache objects Event: 10222, Dtrace Script (I)
  2. Row Cache Objects, Row Cache Latch on Object Type: Plsql vs Java Call (Part-1) (II)
  3. Row Cache Objects, Row Cache Latch on Object Type: Plsql vs Java Call (Part-2) (III)
  4. Row Cache and Sql Executions (IV)
and then attempt to build a mathematical model to assess such system.

Empirical data and model predication demonstrate that such system is hardly to be scalable with the number of CPUs because of Latch contentions.

In a pure row cache object system, theoretically, 3 Processors (CPU); experimentally, 9 Processors are necessitated to (almost) achieve the maximum throughput. Extra Processors can only put up high contentions.

The possible alternatives could be using Oracle basic (Built-in) data types or object string serialization.

Finally, we are trying to show that "row cache objects Latch" falls into the same destiny as the single "Result Cache: RC Latch" discussed in Blog: PL/SQL Function Result Cache Invalidation (I)

As a secondary earning of this exercise, "library cache: mutex X" contentions are observed related to Oracle Object Types.

Note: All tests are done in Oracle 12.1.0.2.0 on Solaris, Linux, AIX (SMT 4, LCPU=24) with 6 physical processors.


1. Test


We will make the test based on the code of Blog: Row Cache Objects, Row Cache Latch on Object Type: Plsql vs Java Call (Part-1) (II)


1.1. Plsql Test


Run (see appended Test Code: Plsql):
exec foo_proc_sessions_ctrl(1e9, 10);
which performs dynamic Plsql Call (foo_proc) for 9 different parallel degrees, each of which is running for 10 minutes.

Then collect the test data by query:
select * from exec_stats_v;
Table below shows number of parallel (concurrent) sessions, total number of executions, cpu_time in seconds, concurrency_wait_time in seconds, and microseconds per execution.
exec_stats_v (Plsql SQL_ID: 241pfd82cu4wj)

SESSIONS   EXECUTIONS CPU_TIME_S CONCURRENCY_WAIT_TIME_S US_PER_EXEC
--------   ---------- ---------- ----------------------- -----------
Solairs        
       1    8,096,318        413                       0          51
       3   21,850,342      1,212                      45          57
       6   32,609,592      1,923                     520          75
      12   42,217,169      2,555                   1,693         125
      18   42,890,535      2,615                   3,548         193
      24   42,955,245      2,631                   5,667         265
      36   43,117,305      2,633                   9,372         406
      42   42,523,671      2,616                  11,551         480
      48   40,546,142      2,506                  13,915         561 

AIX            
       1    6,535,357        251                       0          65
       3   17,355,458        714                      75          74
       6   21,327,093      1,069                     664         117
      12   24,797,761      1,353                   1,814         198
      18   25,037,784      1,740                   2,992         297
      24   20,691,357      2,597                   2,537         542
      36   19,048,665      2,945                   7,872         963
      42   19,199,984      2,872                  11,683       1,132
      48   19,214,011      2,815                  15,315       1,306
               
Linux          
       1   13,992,932        413                       0          30
       3   32,715,487      1,216                      44          39
       6   52,665,062      2,222                     316          48
      12   49,743,420      2,146                   2,321          98
      18   50,448,264      2,249                   4,525         149
      24   50,836,921      2,340                   6,895         201
      36   51,864,133      2,458                  12,002         307
      42   48,914,411      2,495                  14,942         390
      48   48,549,375      2,535                  17,989         460         
The above result shows:
  Throughput (EXECUTIONS) from 1 to 3 SESSIONS climbs approximately linearly; of 6 SESSIONS tends to be flat or descending.
  Max throughput is achieved with around 9 parallel Sessions.
  The performance is saturated by more than 12 parallel Sessions. 
  The response time per execution (US_PER_EXEC) gets increased, probably due to the Latch contentions
    (sessions spend more time on Latch Gets instead of real work).
During the test, we also collect the data for Row Cache Object Gets, and Row Cache Latch Gets.

Here the output for parallel sessions 12 and 42.
select * from rco_stats_v where sessions in (12, 42) order by child#;

rco_stats_v

SESSIONS  CHILD#  RC_PARAMETER      GETS       MISSES    SLEEPS  SPIN_GETS  WAIT_TIME_S  RC_GETS    RC_GETMISSES  RC_THEORY_GETS  CACHE#_LIST  PARAMETER_LIST
--------  ------  ----------------  ---------  --------  ------  ---------  -----------  ---------  ------------  --------------  -----------  ----------------------------
   12       8     dc_users          633984132  19649121  52958   19596500   262          211328141  0             1901952396      7, 10        dc_users, dc_users
   12       9     dc_object_grants  633984845  54784381  58712   54726007   292          211328352  0             1901954535      8, 8         dc_objects, dc_object_grants
   12       10    dc_global_oids    633982830  52451846  68813   52383487   346          211327687  0             1901948490      17           dc_global_oids
            
   42       8     dc_users          638662605  22653084  56234   22597401   2274         212887753  47            1915987815      7, 10        dc_users, dc_users
   42       9     dc_object_grants  638674026  56604969  65236   56540334   3006         212891172  552           1916022078      8, 8         dc_objects, dc_object_grants
   42       10    dc_global_oids    638657482  64387264  76460   64311564   3905         212885997  30            1915972446      17           dc_global_oids
Look the case of 42 SESSIONS, although all 3 Latch Gets are similar, dc_global_oids suffers most, then dc_object_grants, and dc_users least. They are exposed by the different MISSES, SLEEPS, SPIN_GETS, WAIT_TIME. We will use this info in the later causality modeling.

Note Latch GETS is 3 times of RC_GETS.


1.2. Java Test


Run appended shell script:
ksh ./foo_proc_sessions_java 600
which invokes Java (RCOObjTypeJDBC1) to call foo_proc for 9 different parallel degrees, each of which is running for 10 minutes, same as above Plsql Test.

Then collect the similar data for Java Call as Table below.

exec_stats_v (Java SQL_ID: 8nkt65d7hcnxz)

SESSIONS EXECUTIONS CPU_TIME_S  CONCURRENCY_WAIT_TIME_S US_PER_EXEC
-------- ---------- ----------  ----------------------- -----------
Solairs                                                            
      1   3,790,741         294                       0          77
      3   9,154,792         774                     112          99
      6  13,334,385       1,213                     631         149
     12  16,216,973       1,565                   1,711         264
     18  16,971,173       1,720                   2,622         400
     24  17,062,335       1,745                   3,962         551
     36  16,616,651       1,735                   6,995         925
     42  16,492,514       1,733                  10,003       1,117
     48  16,264,599       1,721                  13,106       1,323

AIX                                                                
      1   3,288,798         166                       0          86
      3   8,965,468         498                      39          99
      6  11,661,246         701                     221         148
     12  14,770,591       1,186                     972         274
     18  14,791,463       1,715                   1,534         459
     24  13,030,697       2,220                   1,727         749
     36  12,096,987       2,211                   5,475       1,173
     42  11,845,242       2,090                   8,166       1,388
     48  11,732,373       2,080                  11,252       1,661
                                                                   
Linux                                                              
      1   4,788,119         197                       0          50
      3  12,325,022         647                      22          66
      6  20,053,654       1,233                     261          91
     12  20,646,925       1,050                   1,518         156
     18  20,118,792       1,016                   3,261         257
     24  19,740,172       1,148                   5,525         393
     36  19,285,554       1,229                  10,114         662
     42  19,069,527       1,226                  12,541         806
     48  18,363,626       1,194                  15,031         978                                                                  
The result shows that throughput (EXECUTIONS) is about 2 times lower than Plsql, and response time (US_PER_EXEC) is about 2 times higher than Plsql.

In Blog: Row Cache Objects, Row Cache Latch on Object Type: Plsql vs Java Call (Part-1) (II), we see that each dynamic Plsql foo execution requires 45 Row Cache Latch Gets, whereas that of Java is 63. About 40% more Latch Gets in Java, but its throughput is about 2 times lower than Plsql.


2. Modeling


Let's try to model 3 Row Cache Object (CID: 17, 11, 7) Gets, and respective Latch Gets, each of which contains 3 latch Locations ("Where").

In the following discussion, we model:
  Oracle Session          as Job
  Row Cache Object Get    as Task
  Task Processing Server  as Machine
Suppose we have one Workshop W, and n Job Generators:
  G = (G1, G2, ... ,Gn)
each holds one single Job in each instant (one terminated, a new produced).

Each Job is made of 3 Tasks (sub-Jobs):
  J_i = (Si_1, Si_2, Si_3)
Each Task consists of 3 Work Units:
  Si_1 = (Si_1_u1, Si_1_u2, Si_1_u3)
  Si_2 = (Si_2_u1, Si_2_u2, Si_2_u3)
  Si_3 = (Si_3_u1, Si_3_u2, Si_3_u3)
They are subject to constraints:
  All 3 Tasks in each Job have to be processed sequentially.
  All 3 Work Units in each Task have to be processed sequentially.
  The Tasks and Work Units among different Jobs can be in parallel.
The Workshop is equipped with an assembly line, which consists of 3 Machines (Processors):
  W = (M1, M2, M3)
Each Machine possesses 3 Processing Units:
  M1 = (p1_1, p1_2, p1_3)
  M2 = (p2_1, p2_2, p2_3)
  M3 = (p3_1, p3_2, p3_3)
3 Machines are dedicated respectively for 3 Tasks:
  M1 exclusively processes Si_1
  M2 exclusively processes Si_2
  M3 exclusively processes Si_3
M1, M2, M3 are running in parallel (inter-parallel); but Machine'3 processing Units are hardly running in parallel (no intra-parallel).

The service time of 3 Machines are:
  t1 for M1 to process Si_1
  t2 for M2 to process Si_2
  t3 for M3 to process Si_3 
So minimum processing time of each Job is (t1 + t2 + t3).

Let's look the processing of first n Jobs.

Assume t1 < t2 <t3 (later empirical data support such presumption), after i-th Job being processed, there are:
  (n-i)*(t2-t1)/t1 
Jobs waiting before M2 after M1; There are:
  (n-i)*(t3-t2)/t2  
Jobs waiting before M3 after M2 when i-th Job being processed by M3.

So M3 processing i-th Job caused a delay of
  (t3-t2)
for
  (n-i)*(t3-t2)/t2 
Jobs, that means an accumulation delay by i-th Job (J_i) is:
  (t3-t2) * (n-i) * (t3-t2)/t2    1 <= i <= n 
The total accumulation waiting time before M3 for processing all n Jobs is:
   (t3-t2) * (n*(n-1)/2) * (t3-t2)/t2
  = n*(n-1) * (t3-t2)^2 / (2 * t2)       
In case of t1 < t2 < t3, all Jobs waiting before M3. If we only consider the fastest machine M1 and slowest machine M3, i.e. min(t1, t2, t3) = t1, max(t1, t2, t3) = t3, M1 is first machine, M3 is last one, both are running in parallel, The accumulation waiting time inside whole Workshop can be approximately estimated as:

Average waiting time per Job is:
  (n-1) * (t3-t1)^2 / (2 * t1)
Put all together, average response time can be expressed as:
    avg_response_time (rt) = waiting_time (wt) + service_time (st)
                           = (n-1) * (t3-t1)^2 / (2 * t1) + t3
    
    throughput             = (1 / rt) 
                          <= (1 / t3) 
Therefore,
  total response time is quadratically to n.
  average response time is linearly to n, which is similar to test result.
  maximum throughput is 1 / avg_response_time.    
Take the example 10222 trace file of Solairs at the beginning of this section:
  t1 = 255 us   for cid = 17
  t2 = 324 us   for cid = 11 
  t3 = 410 us   for cid =  7 (covered by cid = 11)
    
  t1 + t2 + t3 = 989 (255+324+410) = sum
The time collected in 10222 trace is much bigger than Solaris US_PER_EXEC for one single session (SESSIIONS=1), probably because overhead of 10222 tracing. So we first convert them to the number without tracing (all are commented after "->").
  US_PER_EXEC = 51  -- Solaris SESSIONS=1 
  
  t1 + t2 + t3 = 989 (255+324+410) = sum -> 51
  
  t1 = 255   -> 51 * 255/989 = 13
  t2 = 324   -> 51 * 324/989 = 17    
  t3 = 410   -> 51 * 410/989 = 21
Now we can try to make the calculation for 12 SESSIONS (n=12). The accumulation waiting time inside entire Workshop is:
  n*(n-1) * (t3-t2)^2 / (2 * t2)        
  = 12*11*(21-13)*(21-13)/(2*13) = 324
Average waiting time for each Job:
  (n-1) * (t3-t2)^2 / (2 * t2) 
  = 11*(21-13)*(21-13)/(2*13) = 27
Average response time for each Job:
  27 + 21 = 48  
In the above discussion, Job is modelled by 2 Layers:
  Task
  Work Unit
Workshop is also modelled by 2 Layers correspondingly:
  Machine 
  Processing Units
Till now, we only assess the first Layer. The second Layer have not yet been taken into account:
  Work Units 
  Processing Units 
Another deficiency is that there are n Job Generators, and each Generator can produce next Job once previous Job is terminated (boundary condition), so there are total n Jobs in the system, but above model only consider one time of processing first n Jobs.

Back to Row Cache, the scope of model is still too far to reflect the real system because we only consider inter-parallel, but no intra-parallel. This is not precise because intra-parallel is related to 2nd and 3rd latch Get Locations; whereas inter-parallel is related to 1st latch Get Locations.

Besides that, the real system is influenced by Latch timeout, spinning, process Preemption, Multithreading, and hardware specialities (e.g. Solaris LWP, AIX SMT).

As evidenced by the model, there exists no deadlock in such a system since all operations are performed sequentially.


3. Model Justification


To verify the model, 3 parameters t1, t2, t3 have to be acquired.

Run foo_proc (see Blog: Row Cache Objects, Row Cache Latch on Object Type: Plsql vs Java Call (Part-1) (II)) with 10222 trace in an idle system.

Then excerpt all 3 Cache Gets (CID: 17, 11, 7) for one Object instance Get, for example, T_OBJ_OUT (This is only illustrative. In practice, more representative data should be captured).

Here is an example from Solaris (irrelevant lines removed):
(12cR1 10222 trace is hard to read, 12cR2 improved. see Blog: Oracle row cache objects Event: 10222, Dtrace Script (I))

--================ Start T_OBJ_OUT Get ================--

kqrfrpo : freed to fixed free list po=175d2f708 time=1459184662

  ##********* cid = 17, Start time=1459184662 *********--
kqrpad: new po 17727bac0 from kqrpre1.1

kqrReadFromDB : kqrpre1.1 po=17727bac0 flg=8000 cid=17 eq=1753067e0 idx=0 dsflg=0
kqrpre1 : done po=17727bac0 cid=17 flg=2 hash=3c4bb9d0 0 eq=1753067e0 SQL=begin :1 := foo(:2, :3, :4); end; time=1459184804

  ##------- cid = 17, End time=1459184917, Elapsed = 255 (1459184917-1459184662) ------- 
kqrfrpo : freed to fixed free list po=17727bac0 time=1459184917

  ##--********* cid = 11, Start time=1459184917, including cid = 7 *********--

kqrpad: new po 175d2f708 from kqrpre1.1

kqrReadFromDB : kqrpre1.3 po=175d2f708 flg=8000 cid=11 eq=1753067e0 idx=0 dsflg=0
kqrpre1 : done po=175d2f708 cid=11 flg=2 hash=94b841cf a9461655 eq=1753067e0 
          obobn=2360170 obname=T_OBJ_OUT obtyp=13 obsta=1 obflg=0 SQL=begin :1 := foo(:2, :3, :4); end; time=1459185132

  ##--********* cid = 7, Start time=1459185132 *********--

kqrpad: new po 170d94488 from kqrpre1.1

kqrReadFromDB : kqrpre1.3 po=170d94488 flg=8000 cid=7 eq=176e410b8 idx=0 dsflg=0
kqrpre1 : done po=170d94488 cid=7 flg=2 hash=de7751cd 395edb55 eq=176e410b8 
          SQL=begin :1 := foo(:2, :3, :4); end; time=1459185415

  ##------- cid = 7, End time=1459185542, Elapsed = 410 (1459185542-1459185132) -------
kqrfrpo : freed to heap po=170d94488 time=1459185542

kqrmupin : kqrpspr2 Unpin po 175d2f708 cid=11 flg=2 hash=a9461655 time=1459185556

  ##------- cid = 11, End time=1459185651, Elapsed = 734 (1459185651-1459184917), Pure = 324 (734-410) -------
kqrfrpo : freed to fixed free list po=175d2f708 time=1459185651

--================ End T_OBJ_OUT Get ================--
Look that example output from Solaris (irrelevant lines removed. AIX and Linux are added for comparison), values for 3 Cache Gets are captured ("time" in microsecond are enclosed in comment lines like <-- xx -->), then convert them into values (all prefixed by "->") without 10222 tracing according to value for SESSIONS=1 (no concurrency) in Table-1.
  
----------------------- Solaris ----------------------
  US_PER_EXEC = 51  -- Table-1 Solaris SESSIONS=1 
  
  t1 = 255 us -> 13  for cid = 17
  t2 = 324 us -> 17  for cid = 11 
  t3 = 410 us -> 21  for cid =  7 (covered by cid = 11)
  
  total Elapsed = 989 us (255+324+410) -> 51
  
----------------------- AIX ----------------------
  US_PER_EXEC = 65  -- Table-1 AIX SESSIONS=1 
  
  t1 = 143 us ->  9  for cid = 17
  t2 = 186 us -> 12  for cid = 11 
  t3 = 252 us -> 16  for cid =  7 (covered by cid = 11)
  
  total Elapsed = 581 us (143+186+252) -> 65  

----------------------- Linux ----------------------
  US_PER_EXEC = 30  -- Table-1 Linux SESSIONS=1 

  t1 = 136 us ->  8  for cid = 17
  t2 = 164 us ->  9  for cid = 11 
  t3 = 229 us -> 13  for cid =  7 (covered by cid = 11)
  
  total Elapsed = 529 us (136+164+229) -> 30  
Using the above average response time formula:
  
  (n-1) * (t3-t1)^2 / (2 * t1) + t3
substitute all variables and run following queries (for SESSIONS varying from 1 to 48), we get the average response time in microsecond (us) per execution: MODEL_US_PER_EXEC. Both test data and model data are shown in Table below.
  
-------- Solaris --------
select * from (
  select level sessions,
         round((level-1)*(21-13)*(21-13)/(2*13)) + 21 model_us_per_exec
  from dual connect by level < 100
) where sessions in (1, 3, 6, 12, 18, 24, 36, 42, 48);

-------- AIX --------
select * from (
  select level sessions,
         round((level-1)*(16-9)*(16-9)/(2*9)) + 16 model_us_per_exec
  from dual connect by level < 100
) where sessions in (1, 3, 6, 12, 18, 24, 36, 42, 48);

-------- Linux --------
select * from (
  select level sessions,
         round((level-1)*(13-8)*(13-8)/(2*8)) + 13 model_us_per_exec
  from dual connect by level < 100
) where sessions in (1, 3, 6, 12, 18, 24, 36, 42, 48);
All tests are done in Oracle 12.1.0.2.0 on Solaris, Linux, AIX (SMT 4, LCPU=24) with 6 physical processors. Linux and AIX are added for comparison.

Parallel | Test       | Test        | Model      
SESSIONS | EXECUTIONS | US_PER_EXEC | US_PER_EXEC
---------| ---------- | ----------- | -----------
Solairs  |            |             |            
       1 |  8,096,318 |          51 |          21
       3 | 21,850,342 |          57 |          26
       6 | 32,609,592 |          75 |          33
      12 | 42,217,169 |         125 |          48
      18 | 42,890,535 |         193 |          63
      24 | 42,955,245 |         265 |          78
      36 | 43,117,305 |         406 |         107
      42 | 42,523,671 |         480 |         122
      48 | 40,546,142 |         561 |         137
---------| ---------- | ----------- | -----------
AIX      |            |             |            
       1 |  6,535,357 |          65 |          16
       3 | 17,355,458 |          74 |          21
       6 | 21,327,093 |         117 |          30
      12 | 24,797,761 |         198 |          46
      18 | 25,037,784 |         297 |          62
      24 | 20,691,357 |         542 |          79
      36 | 19,048,665 |         963 |         111
      42 | 19,199,984 |       1,132 |         128
      48 | 19,214,011 |       1,306 |         144
---------| ---------- | ----------- | -----------
Linux    |            |             |            
       1 | 13,992,932 |          30 |          13
       3 | 32,715,487 |          39 |          16
       6 | 52,665,062 |          48 |          21
      12 | 49,743,420 |          98 |          30
      18 | 50,448,264 |         149 |          40
      24 | 50,836,921 |         201 |          49
      36 | 51,864,133 |         307 |          68
      42 | 48,914,411 |         390 |          77
      48 | 48,549,375 |         460 |          86
Now it is open to judge the model by comparing the empirical observations with model predicated values, and inspect its capability of extrapolation.


4. Row Cache Objects Latch: dedicated


Following query shows that there exists a 1-to-1 mapping between Row Cache and Row Cache Latch. Each Cache is protected by one single dedicated Latch. Hence accessing Row Cache is serialized by its designated Latch.

create view sys.x_kqrst as select * from x$kqrst;

select count(distinct latch_child), count(distinct cache#)
  from (
  select la.child# latch_child, kqrstcid cache#, kqrsttxt parameter, dc.*, la.* 
  from sys.x_kqrst dc, v$latch_children la 
  where dc.kqrstcln = la.child# and la.name='row cache objects'
  )
where kqrsttyp =1
order by latch_child;

COUNT(DISTINCTLATCH_CHILD)  COUNT(DISTINCTCACHE#)
--------------------------  ---------------------
56                          56


select *
  from (
  select la.child# latch_child, kqrstcid cache#, kqrsttxt parameter, kqrsttyp  --, dc.*, la.* 
  from sys.x_kqrst dc, v$latch_children la 
  where dc.kqrstcln = la.child# and la.name='row cache objects'
  )
where kqrsttyp =1   -- parent
order by latch_child;

LATCH_CHILD  CACHE#  PARAMETER              KQRSTTYP
----------- ------- ---------------------  ---------
8            10      dc_users               1
9            8       dc_objects             1
10           17      dc_global_oids         1
18           15      dc_props               1
For example, to access Object Type: T_OBJ_OUT, we should sequentially go though 3 Caches:
  dc_global_oids
  dc_objects
  dc_users
and each of which is guarded by its responsible Child Latch:
  10
  9
  8  
It looks like that "row cache objects Latch" works similar to the single "Result Cache: RC Latch" discussed in Blog: PL/SQL Function Result Cache Invalidation (I). But the difference is that later is optional, and can be turned off; but the former is an unavoidable obstacle.

To this point, we can have a further look of other Oracle Latches. Probably only cache buffers chains Latch is conceived to be scale (nonetheless "latch: cache buffers chains" Wait Event) since each of which has a limited responsibility and it amounts to majority of Latch (> 75%). Any excessive usage of other Latches is rated as irrational, hence contentions.

select l.name, count(*) cnt, round(100*ratio_to_report(count(*)) over (), 2) ratio 
from v$latch l, v$latch_children k
where l.latch# = k.latch#(+) 
--  and l.name in ('cache buffers chains', 'row cache objects', 'shared pool', 'Result Cache: RC Latch', 'kokc descriptor allocation latch')
--  and lower(l.name) like '%kokc%'
group by l.name order by cnt desc, l.name;

NAME                               CNT     RATIO
--------------------------------- ------  ------
cache buffers chains               32768   75.38
simulator hash latch               2048    4.71
...                                        
row cache objects                  56      0.13
...                                        
shared pool                        7       0.02
...                                        
Result Cache: RC Latch             1       0
... 
kokc descriptor allocation latch   1       0

770 rows selected.
(-------------- cache buffers chains --------------
Hidden Parameters and default values: 
  _db_block_hash_latches=32768    Number of database block hash latches,
  _db_block_hash_buckets=1048576  Number of database block hash buckets 
           1048576/32768=32       buckets per latch
 
Blog: Hot Block Identification and latch: cache buffers chains
-------------- simulator hash latch --------------
Blog: Row Cache and Sql Executions (IV)
-------------- row cache objects --------------
"Current Location" 
and
Blog: nls_database_parameters, dc_props, latch: row cache objects
-------------- shared pool --------------
Blog: sql_id and idn to shared pool subpool
-------------- Result Cache: RC Latch --------------
Blog: Result Cache:0 rows updated Invalidations (IV)
-------------- kokc descriptor allocation latch --------------
Bug 14382262  latch free wait event in"kokc descriptor allocation latch"

Blog: dbms_aq.dequeue - latch: row cache objects on AIX
Blog: Shared Pool - KKSSP
)


5. AWR/ASH - Row Cache Objects


We collect AWR/ASH reports for one 10 minutes Run of Plsql Test in 42 parallel sessions, and extract all Row Cache Objects related Sections.


5.1. Load Profile


DB CPU(s) Per Second: 5.9 shows that all 6 CPU are busy (CPU intensive), and about 100*(1-5.9/66.2) = 91% of DB Time are in waiting.

Load Profile
  Per Second Per Transaction Per Exec Per Call
DB Time(s): 66.2 373.6 0 195.01
DB CPU(s): 5.9 33.2 0 17.34


5.2. Top 10 Foreground Events by Total Wait Time


23 %DB time is in Waiting Event for latch: row cache objects, with Wait Avg(ms) 46.85 (probably bigger than 10 ms CPU Scheduling time slice).

9 %DB time is in Waiting Event for library cache: mutex X, with Wait Avg(ms) 10.49.

Top 10 Foreground Events by Total Wait Time
Event Waits Total Wait Time (sec) Wait Avg(ms) % DB time
latch: row cache objects 196,081 9185.8 46.85 23
library cache: mutex X 367,581 3856 10.49 9.6
DB CPU   3554.8   8.9


5.3. SQL ordered by Elapsed Time


SQL_ID: 241pfd82cu4wj has 42,533,404 Executions with Elapsed Time 20,423.63 seconds, all in CPU.

SQL ordered by Elapsed Time
Elapsed Time (s) Executions Elapsed Time per Exec (s) %Total %CPU %IO SQL Id SQL Text
24,895.60 42 592.75 62.27 14.26 0 22thkvvjndnmj DECLARE job BINARY_INTEGER := ...
20,423.63 42,533,404 0 51.09 12.81 0 241pfd82cu4wj begin :1 := foo(:2, :3, :4); e...


5.4. Latch Activity and Latch Sleep Breakdown


Total Latch Gets is 1,916,333,412, with 197,952 Sleeps, about 7.5% of Latch Miss (Gets/Misses=143,650,808/1,916,333,412). 197,952 Sleeps matches Latch Waits in Section "Top 10 Foreground Events by Total Wait Time".

Look rco_stats_v table for 42 SESSIONS, the 3 GETs for 3 Caches (dc_global_oids, dc_object_grants, dc_users) summing together is 1,915,994,113, close to above Total Latch Gets.

Same matches for MISSES, SLEEPS, SPIN_GETS, WAIT_TIME_S.

Latch Activity
Latch Name Get Requests Pct Get Miss Avg Slps /Miss Wait Time (s)
row cache objects 1,916,333,412 7.5 0 9186

Latch Sleep Breakdown
Latch Name Get Requests Misses Sleeps Spin Gets
row cache objects 1,916,333,412 143,650,808 197,952 143,454,769


5.5. Latch Miss Sources


Blog: Oracle row cache objects Event: 10222, Dtrace Script (I) shows that each Latch Get goes through 3 consecutive Locations ("Where"):
  Where=>4441(0x1159): kqrpre: find obj   -- latch Get at 1st Location
  Where=>4464(0x1170): kqreqd             -- latch Get at 2nd Location
  Where=>4465(0x1171): kqreqd: reget      -- latch Get at 3rd Location
Here we can see "kqrpre: find obj" is main blocker (Waiter Sleeps: 179,972), whereas "kqreqd: reget" is the top victim (Sleeps: 157,629), which accounts for about 80% (157,629/197,952) of Latch Sleeps.

In the above discussion, we noted that inter-parallel is related to 1st Latch Get Locations.

Latch Miss Sources
Latch Name Where NoWait Misses Sleeps Waiter Sleeps
row cache objects kqreqd: reget 0 157,629 3,121
row cache objects kqrpre: find obj 0 33,484 179,972
row cache objects kqreqd 0 6,812 14,827


5.6. Mutex Sleep Summary


This is related to library cache: mutex X (to be discussed in next Section).

Mutex Sleep Summary
Mutex Type Location Sleeps Wait Time (ms)
Library Cache kglpndl1 95 163,409 1,187,289
Library Cache kglhdgn2 106 161,608 1,191,330
Library Cache kglpin1 4 58,848 393,368
Library Cache kgllkdl1 85 31,551 186,930
Library Cache kglpnal1 90 30,451 176,736
Library Cache kglget1 1 30,157 233,526
Library Cache kglhdgn1 62 26,839 229,241


5.7. Child Latch Statistics


See rco_stats_v.

Child Latch Statistics
Latch Name Child Num Get Requests Misses Sleeps Spin & Sleeps 1->3+
row cache objects 9 638,804,551 56,606,984 65,237 56542348/0/0/0
row cache objects 8 638,793,715 22,654,054 56,248 22598358/0/0/0
row cache objects 10 638,788,857 64,389,752 76,467 64314045/0/0/0


5.8. Dictionary Cache Stats


See rco_stats_v.

I almost overlooked this Section because each popular Oracle term gets two names.

Dictionary Cache Stats
Cache Get Requests Pct Miss Scan Reqs Pct Miss Mod Reqs Final Usage
dc_global_oids 212,933,433 0 0   0 69
dc_objects 212,938,294 0 0   117 3,585
dc_users 212,935,101 0 0   0 89


5.9. Top SQL with Top Events, Top Events


This ASH Section listed our foo statement triggering the Top Events.

Top SQL with Top Events
SQL ID Plan Hash Executions % Activity Event % Event SQL Text
241pfd82cu4wj   2048 84.37 latch: row cache objects 41.39 begin :1 := foo(:2, :3, :4); e...
241pfd82cu4wj   2048 84.37 CPU + Wait for CPU 34.99 begin :1 := foo(:2, :3, :4); e...
241pfd82cu4wj   2048 84.37 library cache: mutex X 7.92 begin :1 := foo(:2, :3, :4); e...

Top Events
Event Event Class Session Type % Activity Avg Active Sessions
CPU + Wait for CPU CPU FOREGROUND 45.57 18.39
latch: row cache objects Concurrency FOREGROUND 41.39 16.71
library cache: mutex X Concurrency FOREGROUND 12.92 5.22


5.10. Top Event P1/P2/P3 Values



Top Event P1/P2/P3 Values
Event % Event P1, P2, P3 Values % Activity Parameter 1 Parameter 2 Parameter 3
latch: row cache objects 41.39 "6490258456","411","0" 18.91 address number tries
latch: row cache objects   "6490257608","411","0" 11.94 address number tries
latch: row cache objects   "6490258032","411","0" 10.54 address number tries
library cache: mutex X 12.92 "595386336", "3143916060672", "10136848668098666" 0.66 idn value where

Pick all 3 P1 Parameters in Event latch, run query below. The output shows that they point to the 3 Child_Latches (10, 9, 8) repectively.

select to_number(addr, 'XXXXXXXXXXXXXXXX') addr_num, addr,
       child#, gets, misses, sleeps, spin_gets, wait_time
 from v$latch_children lat
where latch#=411 
  and (addr like '%'||trim(to_char(6490258456, 'XXXXXXXXX'))
    or addr like '%'||trim(to_char(6490257608, 'XXXXXXXXX'))
    or addr like '%'||trim(to_char(6490258032, 'XXXXXXXXX')))
order by sleeps desc;

ADDR_NUM    ADDR              CHILD#  GETS       MISSES    SLEEPS  SPIN_GETS  WAIT_TIME
----------  ----------------  ------  ---------  --------  ------  ---------  ----------
6490258456  0000000182D97C18  10      638829560  64389752  76467   64314045   3905162356
6490258032  0000000182D97A70  9       638989001  56607014  65237   56542378   3006243205
6490257608  0000000182D978C8  8       639105230  22654062  56248   22598366   2274407991


6. library cache: mutex X


Look ASH Section - Top Event P1/P2/P3 Values, pick P1 Parameters in Event mutex, run query below.

The output shows that P1 Parameter: "595386336" represents T_OBJ_IN.

with start_snap as (
  select mutex_identifier, location, max(gets) gets, max(sleeps) sleeps, min(sleep_timestamp) min_ts
   from v$mutex_sleep_history
  where sleep_timestamp < sysdate -30/1440 
   group by mutex_identifier, location)
 ,end_snap as (
  select mutex_identifier, location, min(gets) gets, min(sleeps) sleeps, max(sleep_timestamp) max_ts
   from v$mutex_sleep_history
  where sleep_timestamp < sysdate -1/1440 
   group by mutex_identifier, location)
 ,delta as (
  select e.mutex_identifier, e.location, 
       e.gets   - nvl(s.gets, 0)   gets,
       e.sleeps - nvl(s.sleeps, 0) sleeps, min_ts, max_ts
  from start_snap s, end_snap e
  where e.mutex_identifier = s.mutex_identifier (+) 
    and e.location = s.location (+))
 ,snap as (
  select mutex_identifier, sum(gets) gets, sum(sleeps) sleeps, min(min_ts) min_ts, max(max_ts) max_ts
    from delta group by mutex_identifier
   order by sleeps desc
  )  
select o.hash_value, name, s.gets, s.sleeps, s.min_ts, s.max_ts
 from v$db_object_cache o, snap s
where o.hash_value = s.mutex_identifier
--  and mutex_identifier = 595386336
  and rownum <= 5
order by s.sleeps desc;

HASH_VALUE  NAME         GETS        SLEEPS
----------  -----------  ----------  ------
2812823600  T_OBJ_RET    1063400668  19362
668251247   T_OBJ_INOUT  1276396207  13660
595386336   T_OBJ_IN     1063241644   9290
415170529   T_OBJ_OUT    1063009333   8719
4198378475  FOO           254328227   1902
By the above output, if T_OBJ_IN occupies to 0.66 %Activity, T_OBJ_RET could be estimated as 1.38% (0.66*19362/9290) %Activity. Probably not always the TOP consumer is displayed in ASH.

Following query would give an output similar to AWR Section: Mutex Sleep Summary.
(The data in v$mutex_sleep_history is contained within a circular buffer, with the most recent sleeps shown)

with start_snap as (
  select mutex_identifier, location, max(gets) gets, max(sleeps) sleeps
   from v$mutex_sleep_history
  where sleep_timestamp < timestamp'2018-12-17 08:04:30' 
   group by mutex_identifier, location)
 ,end_snap as (
  select mutex_identifier, location, min(gets) gets, min(sleeps) sleeps
   from v$mutex_sleep_history
  where sleep_timestamp < timestamp'2018-12-17 08:14:50'
   group by mutex_identifier, location)
 ,delta as (
  select e.mutex_identifier, e.location, 
       e.gets   - nvl(s.gets, 0)   gets,
       e.sleeps - nvl(s.sleeps, 0) sleeps
  from start_snap s, end_snap e
  where e.mutex_identifier = s.mutex_identifier (+) 
    and e.location = s.location (+))
select location, sum(gets) gets, sum(sleeps) sleeps
  from delta group by location
 order by sleeps desc;
 
LOCATION                    GETS        SLEEPS
--------------------------  ----------  ------
kglhdgn2 106                977757576   13812
kglpndl1  95                1062042442  12725
kglpin1   4                 1020903933  11455
kglpnal1  90                 680931048   9432
kgllkdl1  85                 425439695   5679
kksLockDelete [KKSCHLPIN6]   425095566   3199
kglget1   1                  255195587   2600
kglhdgn1  62                 336370181   2466
kksxsccmp [KKSCHLPIN5]       169434152    242
kksfbc [KKSCHLFSP2]          169595310    232
A quick alternative is to create multiple copies of hot objects, for example, for T_OBJ_IN, and T_OBJ_RET (See Blog: "library cache: mutex X" and Application Context)

alter system set "_kgl_hot_object_copies"= 255 scope=spfile;

alter system set "_kgl_debug"=
      "name='T_OBJ_IN'   schema='K'    namespace=1  debug=33554432", 
      "name='T_OBJ_RET'  schema='K'    namespace=1  debug=33554432"
      scope=spfile;


7. Test Code: Plsql



drop sequence foo_seq;

create sequence foo_seq;

create or replace force view rco_stats_record_v as
with rc as (
  select min(parameter) rc_parameter, sum(gets) rc_gets, sum(getmisses) rc_getmisses, 
         decode(substr(parameter, 1, 9), 'dc_users', 8, 'dc_object', 9, 'dc_global', 10) la_child#,
         listagg(cache#, ', ') within group (order by cache#) cache#_list,
         listagg(parameter, ', ') within group (order by length(parameter)) parameter_list
  from   v$rowcache where cache# in (7,10, 8,11, 17) and gets > 0 
  group by substr(parameter, 1, 9))
select localtimestamp ts, addr, child#, name, gets, misses, sleeps, spin_gets, wait_time, rc.*, 3*gets RC_theory_gets
from v$latch_children la, rc
where la.latch# = 411 and la.child# in (8, 9, 10) 
  and la.child# = rc.la_child# 
order by la.child#; 

drop table rco_stats;

create table rco_stats as select 0 run, 0 sessions, v.* from rco_stats_record_v v where 1=2;

create or replace force view rco_stats_v  as
select run, sessions, ts, child#, rc_parameter
      ,gets - lag(gets) over (partition by rc_parameter order by run, sessions) gets
      ,misses - lag(misses) over (partition by rc_parameter order by run, sessions) misses
      ,sleeps - lag(sleeps) over (partition by rc_parameter order by run, sessions) sleeps
      ,spin_gets - lag(spin_gets) over (partition by rc_parameter order by run, sessions) spin_gets
      ,round((wait_time - lag(wait_time) over (partition by rc_parameter order by run, sessions))/1e6) wait_time_s
      ,rc_gets - lag(rc_gets) over (partition by rc_parameter order by run, sessions) rc_gets
      ,rc_getmisses - lag(rc_getmisses) over (partition by rc_parameter order by run, sessions) rc_getmisses
      ,rc_theory_gets - lag(rc_theory_gets) over (partition by rc_parameter order by run, sessions) rc_theory_gets
      ,cache#_list, parameter_list
from rco_stats v;


-- Plsql  241pfd82cu4wj    begin :1 := foo(:2, :3, :4); end; 
-- Java   8nkt65d7hcnxz    BEGIN :1 := K.FOO(:2, :3, :4); END;

create or replace force view exec_stats_record_v as
select localtimestamp ts, sql_id, executions, elapsed_time, cpu_time, concurrency_wait_time
from v$sqlarea v 
where (lower(sql_text) like 'begin :1 := %foo(:2, :3, :4); end;%' or sql_id in ('241pfd82cu4wj', '8nkt65d7hcnxz'))
  and last_active_time = (select max(last_active_time) from v$sqlarea where sql_id in ('241pfd82cu4wj', '8nkt65d7hcnxz'))
  and rownum = 1
order by v.last_active_time desc;     

drop table exec_stats;

create table exec_stats as select 0 run, 0 sessions, v.* from exec_stats_record_v v where 1=2;

create or replace force view exec_stats_v as
select run, sessions, ts
      ,executions - lag(executions) over (order by run, sessions) executions
      ,round((elapsed_time - lag(elapsed_time) over (order by run, sessions))/1e6) elapsed_time_s
      ,round((cpu_time - lag(cpu_time) over (order by run, sessions))/1e6) cpu_time_s
      ,round((concurrency_wait_time - lag(concurrency_wait_time) over (order by run, sessions))/1e6) concurrency_wait_time_s
      ,round((elapsed_time - lag(elapsed_time) over (order by run, sessions))/(nullif (executions - lag(executions) over (order by run, sessions), 0))) us_per_exec
      ,sql_id
from exec_stats;

create or replace procedure record_stats (p_sessions number) as 
  l_run   number;
begin
  l_run := foo_seq.nextval;
  insert into rco_stats  select l_run, p_sessions, v.* from rco_stats_record_v v;
  insert into exec_stats select l_run, p_sessions, v.* from exec_stats_record_v v;
  commit;
  sys.dbms_workload_repository.create_snapshot('ALL');  
end;
/

create or replace procedure k.foo_proc_sessions(p_session_cnt number, p_cnt number) as
  l_job_id pls_integer;
begin
  for i in 1.. p_session_cnt loop
    --dbms_job.submit(l_job_id, 'begin foo_proc('||p_cnt||'); end;');
    dbms_job.submit(l_job_id, 'begin while true loop foo_proc('||p_cnt||'); end loop; end;');
  end loop;
  commit;
end;
/

--exec foo_proc_sessions(6, 1e9);

--exec clean_jobs;

create or replace procedure k.foo_proc_sessions_ctrl(p_cnt number, p_dur_min number := 10) as
  type    t_num_tab is table of number;
  l_nums  t_num_tab := new t_num_tab(1, 3, 6, 12, 18, 24, 36, 42, 48);
  l_run   number;
begin
  foo_proc(10);   -- warmup
  clean_jobs; 
  record_stats(0);
  
  for n in 1..l_nums.count loop
    foo_proc_sessions(l_nums(n), p_cnt);    -- n Sessions
    dbms_lock.sleep(p_dur_min*60);          -- m minutes
    clean_jobs; 
    record_stats(l_nums(n)); 
  end loop;
end;
/

-- exec foo_proc_sessions_ctrl(1e9, 10);


8. Test Code: UNIX Script for Java Test



-------------- create Script: foo_proc_sessions_java ------
#!/bin/ksh

# setup CLASSPATH
export CLASSPATH=$CLASSPATH:java-path:jdbc-path:RCOObjTypeJDBC1-path

function db_record_stats {
  `sqlplus -s /nolog << EOF
  connect k/k
  exec k.record_stats($1);
  exit;
  EOF`  
}

# warm up
/usr/java/bin/java -Xmx30m -Xms8m RCOObjTypeJDBC1 "jdbc:oracle:thin:k/s@testdb:1521:testdb"  10 2
sleep 2

# MOS (Doc ID 2336567.1). new Oracle Database 12c installation.
# ./foo_proc_sessions_java[20]: db_record_stats[9]: PL/SQL: not found [No such file or directory]

db_record_stats 0

for i in 1 3 6 12 18 24 36 42 48
do
  echo "Sessions Count: $i"
  j=1
  while [[ $j -le $i ]]
  do
    echo "Run: $j"
    /usr/java/bin/java -Xmx30m -Xms8m RCOObjTypeJDBC1 "jdbc:oracle:thin:k/s@testdb:1521:testdb"  1000000000 2 &
    (( j = j + 1 ))
  done
  
  sleep $1   # run interval
  
  for pid in $(ps -ef | awk '/RCOObjTypeJDBC1/ {print $2}')
  do 
      if [ -n "$pid" -a -e /proc/$pid ]; then
          echo "Kill Pid: $pid"   # kill if exist
          kill -9 $pid 
      fi    
  done
  sleep 2     # grace period for killing 
  
  db_record_stats $i
done


-------------- called by ----
chmod a+x ./foo_proc_sessions_java
ksh ./foo_proc_sessions_java 600