Monday, September 1, 2025

Oracle Database Stuck on DB Writer Wait Event: "oradebug request completion"

Blog:
    Perilous New Undocumented Tracing Feature in Oracle 21c
    oradebug request completion
revealed DBWR wait event "oradebug request completion" and made an in-depth investigation.

This Blog will demonstrate the reproducing of this event and explore further internals.

Note: Tested on Oracle 19.27 with:
   db_writer_processes = 3
   disk_asynch_io      = true
   aio-max-nr          = 1048576   (for 1048576/4096 = 256 Oracle processes in Linux)

1. Test Setup


1.1 Tablespace, table, index


We create a wide table of 100 rows on a slow file system with each data block containing one single row.

-- Create a file system of 10MB (20480 512-bytes sectors) with 5000 ms read/write delay
--   [root@test_db3] > echo "0 20480 delay /dev/loop3 0 5000" | dmsetup create dm-slow3
--   [root@test_db3] > mount /dev/mapper/dm-slow3 /home/oracle/suk/slow_dir3

drop tablespace test_tbs_slow3 including contents and datafiles cascade constraints;

create SMALLFILE tablespace test_tbs_slow3 datafile
  '/home/oracle/suk/slow_dir3/test_tbs_slow3.dbf' size 5M autoextend on next 1M maxsize 9M
logging online extent management local autoallocate blocksize 8k
segment space management auto flashback on;

  -- Elapsed: 00:02:28.63

drop table test_tab_slow3 cascade constraints;

create table test_tab_slow3(id number, text1 varchar2(3510), text2 varchar2(3510))
tablespace test_tbs_slow3 storage (initial 16k next 128k);

create index test_tab_slow3#id on test_tab_slow3(id) tablespace test_tbs_slow3;

insert into test_tab_slow3 select level id, rpad('ABC', 3000, 'X') text1, rpad('ABC', 3000, 'X') text2 from dual connect by level <= 100; 

commit;


1.2 Procedures


Then create job and "flush buffer_cache" procedures:

create or replace procedure update_row3(p_job_id number, p_loop_count number) as
begin
  for i in 1..p_loop_count loop
    update test_tab_slow3 set text1 = rpad('ABC', 3500, 'X')||1, text2 = rpad('ABC', 3500, 'X')||i where id between (p_job_id-1)*10 and p_job_id*10-1;
    --dbms_session.sleep(0.1);
    commit;
    --execute immediate 'alter system checkpoint';
  end loop;
end;
/

-- exec update_row3(1, 1);

create or replace procedure update_row_jobs3(p_job_count number, p_loop_count number) as
begin
  for i in 1..p_job_count loop
    dbms_scheduler.create_job (
      job_name        => 'TEST_JOB3_'||i,
      job_type        => 'PLSQL_BLOCK',
      job_action      => 'begin update_row3('||i||', '||p_loop_count||'); end;',    
      start_date      => systimestamp,
      --repeat_interval => 'systimestamp',
      auto_drop       => true,
      enabled         => true);
  end loop;
end;
/

--exec update_row_jobs3(10, 10000);
--clearup_test 

create or replace procedure flush_bc (p_cnt number) as
begin
  for i in 1..p_cnt loop
    execute immediate 'alter system flush buffer_cache';
  end loop;
end;
/

-- exec flush_bc(1000);


1.3 Slow I/O Test


Run following code with Sql Trace:

alter system flush buffer_cache;
alter session set tracefile_identifier = 'slow_trc_1';		
alter session set events '10046 trace name context forever, level 12';  
select * from test_tab_slow3 where id = 3;
alter session set events '10046 trace name context off';
Here the row trc and TKPROF file. There are two disk reads (one for index, one for table row), each "db file sequential read" takes 5 seconds.

*** 2025-08-28T10:47:16.022188+02:00
WAIT #140226820024472: nam='db file sequential read' ela= 5176667 file#=2607 block#=139 blocks=1 obj#=5938335 tim=8556907488219
*** 2025-08-28T10:47:21.141086+02:00
WAIT #140226820024472: nam='db file sequential read' ela= 5118663 file#=2607 block#=133 blocks=1 obj#=5938334 tim=8556912607124


SQL ID: c6b234hzgd4u9 Plan Hash: 745759405

select * from test_tab_slow3 where id = 3

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.00      10.29          2          3          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.00      10.29          2          3          0           1

Rows (1st)  Row Source Operation
----------  ---------------------------------------------------
         1  TABLE ACCESS BY INDEX ROWID BATCHED TEST_TAB_SLOW3 (cr=3 pr=2 pw=0 time=10295880 us starts=1 cost=1 size=337428 card=84)
         1   INDEX RANGE SCAN TEST_TAB_SLOW3#ID (cr=2 pr=1 pw=0 time=5177141 us starts=1 cost=1 size=0 card=34)(object id 5938335)

Elapsed times include waiting on following events:
  Event waited on                             Times   Max. Wait  Total Waited
  ----------------------------------------   Waited  ----------  ------------
  SQL*Net message to client                       2        0.00          0.00
  Disk file operations I/O                        1        0.00          0.00
  db file sequential read                         2        5.17         10.29
  SQL*Net message from client                     2        0.07          0.07
********************************************************************************


2. Test Run


2.1 Test Start


We launch 10 table update JOBs and then "flush buffer_cache" to force DB Writers to actively write data to disk.

declare
  l_job_cnt         number := 10;
  l_running_job_cnt number := 0;
begin
   update_row_jobs3(l_job_cnt, 100000);
   
   while l_running_job_cnt < l_job_cnt loop
     select count(*) into l_running_job_cnt from dba_scheduler_jobs where job_name like '%TEST_JOB%';
     dbms_session.sleep(1);
   end loop;
   dbms_session.sleep(4);
   flush_bc(1000);
end;
/


2.2 DBW0 SIGUSR2 signal Catching


We compose one GDB script and use it to catch SIGUSR2 on DBW0:

------------- gdb_dbw0_script_1.txt -------------
set pagination off
set logging file gdb_dbw0_script_1.log
set logging overwrite on
set logging on
set debug timestamp
set $sig_cnt = 0

# we can also use: catch signal SIGUSR2
break sigprocmask
commands
shell date
printf "===== sigprocmask (%i)=====\n", ++$sig_cnt
bt 8
shell sleep 30
cont
end
cont
Here the output when DBW0 receives SIGUSR2:

$ > gdb -x gdb_dbw0_script_1.txt -p 604093

Breakpoint 1 at 0x7fdfa1303630

Program received signal SIGUSR2, User defined signal 2.
0x0000000001f72ce9 in kcbblwr ()
(gdb) c
Continuing.

Breakpoint 1, 0x00007fdfa1303630 in sigprocmask () from /lib64/libc.so.6
Tue Aug 26 20:52:15 CEST 2025
===== sigprocmask (1)=====
#0  0x00007fdfa1303630 in sigprocmask () from /lib64/libc.so.6
#1  0x00000000035e2900 in sspuser ()
#2  
#3  0x0000000001f72ce9 in kcbblwr ()
#4  0x0000000001f5e5bd in kcbbdrv ()
#5  0x00000000137cdf55 in ksb_act_run_int ()
#6  0x00000000137cce04 in ksb_act_run ()
#7  0x000000000407b4be in ksbabs ()

Breakpoint 1, 0x00007fdfa1303630 in sigprocmask () from /lib64/libc.so.6
Tue Aug 26 20:52:45 CEST 2025
===== sigprocmask (2)=====
#0  0x00007fdfa1303630 in sigprocmask () from /lib64/libc.so.6
#1  0x00000000035e296d in sspuser ()
#2  
#3  0x0000000001f72ce9 in kcbblwr ()
#4  0x0000000001f5e5bd in kcbbdrv ()
#5  0x00000000137cdf55 in ksb_act_run_int ()
#6  0x00000000137cce04 in ksb_act_run ()
#7  0x000000000407b4be in ksbabs ()


2.3 DB alert.log Watching


When DBW0 (session ID 810 serial# 24481 OSPID 604093) receives SIGUSR2, DB hangs and becomes unresponsive with DBW0 as Final blocker.
Other DB writers (DBW1 and DBW2) shows "executing after 5 secs due to non-DBWR task".

$ > tail -f alert_test_db3.log

2025-08-26T20:38:35.835071+02:00
DIA0 Critical Database Process As Root: Hang ID 9 blocks 10 sessions
     Final blocker is session ID 810 serial# 24481 OSPID 604093 on Instance 1
     If resolvable, instance eviction will be attempted by Hang Manager
2025-08-26T20:38:40.848025+02:00
System State dumped to trace file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_dia0_604091_base_1.trc
2025-08-26T20:39:23.354993+02:00
System State dumped to trace file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_dia0_604091_lws_2.trc
2025-08-26T20:39:45.434724+02:00
System State dumped to trace file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_dia0_604091_lws_2.trc
2025-08-26T20:40:24.557175+02:00
System state dump requested by (instance=1, osid=604097 (DBW1)), summary=[system state dump request (ksudss)].
System State dumped to trace file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_diag_604074_20250826204024.trc
2025-08-26T20:40:24.561550+02:00
DBW 1 executing after 5 secs due to non-DBWR task at ts=1756233624
2025-08-26T20:40:33.925273+02:00
DBW 2 executing after 5 secs due to non-DBWR task at ts=1756233633
2025-08-26T20:46:13.941192+02:00


2.4 bpftrace tracing tgkill (sender, receiver and signal)


We trace tgkill (sender, receiver, signal) with following script.

A comment like "# DIA0 -> DBW0" is added in the output to denote "DIA0 sends SIGUSR2 (sig: 12) to DBW0".
(In the output, we only show one full callstack of first event).

"# DIA0 -> DBW0" is triggered by "oradebug short_stack" from DIA0 on DBW0.
If DBW0 is slow or there are a huge write, DIA0 session shows Wait Event: "oradebug request completion".
If it takes more than 5 seconds, v$system_event.total_timeouts increased 1.

We can see different pairs of (sender, receiver) like "DBW1 -> CKPT", "DBW0 -> DBW2".
It looks like some automatic diagnostic activities among Oracle processes.

$> bpftrace -e 'BEGIN {@seq = 0}
uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:sskgp_tgkill+2
{
  time("%H:%M:%S --- ");
  printf("%d --- comm=%s pid=%d rsi(receiver_pid)=%d rdx(sig)=%d \n", @seq++, comm, pid, reg("si"), reg("dx"));
  printf("%s\n", ustack())
}'
Attaching 2 probes...

20:37:22 --- 65 --- comm=ora_dia0_test_d pid=604091 rsi(receiver_pid)=604093 rdx(sig)=12      # DIA0 -> DBW0
        sskgp_tgkill+2
        ksdx_cmdreq_issue+644
        ksdx_cmdreq_reissue_attempt+433
        ksdx_cmdreq_exec_track_debugee+517
        ksdx_cmdreq_exec+173
        ksdxdocmdmultex+1768
        ksdxdocmdmult+55
        ksudmp_proc_short_stack+373
        kjznlwshngsew+2725
        kjznlwstrigdmps+268
        kjznlwschksess+1303
        kjznrmnscanrmnsess+11216
        kjznrmnhmdetect+283
        kjznrmnhm+27
        kjznhm+1892
        kjzdregf+906
        kjzdm+691
        ksbrdp+1167
        opirip+541
        opidrv+581
        sou2o+165
        opimai_real+173
        ssthrdmain+417
        main+256
        __libc_start_main+243
        0x5541f689495641d7
        
20:37:37 --- 66 --- comm=ora_dia0_test_d pid=604091 rsi(receiver_pid)=620881 rdx(sig)=12      # DIA0 -> J008
20:40:19 --- 74 --- comm=ora_dbw1_test_d pid=604097 rsi(receiver_pid)=604109 rdx(sig)=12      # DBW1 -> CKPT
20:40:55 --- 94 --- comm=ora_diag_test_d pid=604074 rsi(receiver_pid)=604097 rdx(sig)=12      # DIAG -> DBW1
...
20:51:53 --- 167 --- comm=ora_mmon_test_d pid=604139 rsi(receiver_pid)=604043 rdx(sig)=12     # MMON -> PMON
...
20:53:09 --- 184 --- comm=ora_dia0_test_d pid=604091 rsi(receiver_pid)=604139 rdx(sig)=12     # DIA0 -> MMON
20:53:29 --- 186 --- comm=ora_dbw0_test_d pid=604093 rsi(receiver_pid)=604097 rdx(sig)=12     # DBW0 -> DBW1     
20:53:30 --- 187 --- comm=ora_dbw0_test_d pid=604093 rsi(receiver_pid)=604101 rdx(sig)=12     # DBW0 -> DBW2
20:53:31 --- 202 --- comm=ora_diag_test_d pid=604074 rsi(receiver_pid)=604091 rdx(sig)=12     # DIAG -> DIA0


2.5 bpftrace on DB Writers


We use following script to trace DBWR signal catching (only full callstack are showed for first two events).
It shows DBWR receive signals.

[root@test_db3] >bpftrace -e 'BEGIN {@seq = 0}
tracepoint:syscalls:sys_enter_rt_sigprocmask /strncmp("ora_dbw", comm, 7) == 0/
{
  time("%H:%M:%S --- ");
  printf("%d --- comm=%s pid=%d \n", @seq++, comm, pid);
  printf("%s\n", ustack())
}'
Attaching 2 probes...

19:53:53 --- 0 --- comm=ora_dbw1_test_d pid=604097
        __GI___sigprocmask+17
        0x7f80476f1c20
        skgpwwait+187
        ksliwat+2293
        kslwaitctx+200
        ksarcv+376
        ksbabs+670
        ksbrdp+1167
        opirip+541
        opidrv+581
        sou2o+165
        opimai_real+173
        ssthrdmain+417
        main+256
        __libc_start_main+243
        0x5541f689495641d7
19:53:53 --- 1 --- comm=ora_dbw1_test_d pid=604097
        pthread_sigmask+74
        ksdxcb+499
        sspuser+223
        0x7f80476f1c20
        skgpwwait+187
        ksliwat+2293
        kslwaitctx+200
        ksarcv+376
        ksbabs+670
        ksbrdp+1167
        opirip+541
        opidrv+581
        sou2o+165
        opimai_real+173
        ssthrdmain+417
        main+256
        __libc_start_main+243
        0x5541f689495641d7
20:40:24 --- 3 --- comm=ora_dbw2_test_d pid=604101
        __GI___sigprocmask+17
20:40:30 --- 4 --- comm=ora_dbw2_test_d pid=604101
        pthread_sigmask+74
20:52:45 --- 15 --- comm=ora_dbw0_test_d pid=604093
        __GI___sigprocmask+17
20:52:50 --- 16 --- comm=ora_dbw0_test_d pid=604093
        pthread_sigmask+74


2.6 wait_event "oradebug request completion" systemstate dump


During test, we also set event to generate a system-state trace when a wait of bigger than 10ms on 'oradebug request completion' is detected:

ALTER SYSTEM SET events = 'wait_event["oradebug request completion"]{wait: minwait=10000} systemstate(266)';
--ALTER SYSTEM SET events = 'wait_event["oradebug request completion"]{wait: minwait=10000} off';


SQL> oradebug setmypid
Statement processed.
SQL> oradebug eventdump system
wait_event ["oradebug request completion"] {wait: minwait=10000} systemstate(266)
In DB alert.log, we can see the line:

System State dumped to trace file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_diag_604074_20250826204024.trc
Open "test_db3_diag_604074_20250826204024.trc", it shows DB is blocked by DBW0 (sid 810) and CKPT (sid: 811):

--------------- test_db3_diag_604074_20250826204024.trc ---------------

Unix process pid: 604074, image: oracle@test_db3 (DIAG)
*** 2025-08-26T20:40:24.557417+02:00

System state caller stack:
Short stack dump:
ksedsts()+426<-ksdxfstk()+63<-ksdxdocmdmultex()+5627<-ksdxdocmdmult()+55<-ksudmp_proc_short_stack()+373<-ksudss_opt()+307<-kjzddss()+545<-kjzdmdrq()+6930<-kjzdregf()+4183<-kjzdm()+
691<-ksbrdp()+1167<-opirip()+541<-opidrv()+581<-sou2o()+165<-opimai_real()+173<-ssthrdmain()+417<-main()+256<-__libc_start_main()+243<-_start()+46
===================================================
SYSTEM STATE (level=10, with short stacks)
------------
Dumping the Process Summary

       8: USER ospid 619494 sid 26 ser 53200, waiting for 'DLM cross inst call completion'
          Cmd: PL/SQL EXECUTE
          Blocked by inst: 1, sid: 811, ser: 4896
          Final Blocker inst: 1, sid: 811, ser: 4896
      
      11: DIAG ospid 604074 sid 808 ser 30814,

      18: DIA0 ospid 604091 sid 541 ser 16758, waiting for 'DIAG idle wait'
      19: DBW0 ospid 604093 sid 810 ser 24481,
      20: DBW1 ospid 604097 sid 5 ser 1428, waiting for 'oradebug request completion'
      21: DBW2 ospid 604101 sid 273 ser 22218, waiting for 'rdbms ipc message'
      22: LGWR ospid 604105 sid 542 ser 2191, waiting for 'rdbms ipc message'
      23: CKPT ospid 604109 sid 811 ser 4896, waiting for 'rdbms ipc message'
      24: LG00 ospid 604113 sid 6 ser 7534, waiting for 'LGWR worker group idle'
      25: SMON ospid 604115 sid 274 ser 52613, waiting for 'enq: CR - block range reuse ckpt'
          Blocked by inst: 1, sid: 811, ser: 4896
          Final Blocker inst: 1, sid: 811, ser: 4896
      26: LG01 ospid 604119 sid 543 ser 61931, waiting for 'LGWR worker group idle'
      
      56: J000 ospid 620865 sid 21 ser 42342, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPDATE
          Blocked by inst: 1, sid: 810, ser: 24481
          Final Blocker inst: 1, sid: 810, ser: 24481
      57: J001 ospid 620867 sid 292 ser 4060, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPDATE
          Blocked by inst: 1, sid: 810, ser: 24481
          Final Blocker inst: 1, sid: 810, ser: 24481
      58: J002 ospid 620869 sid 560 ser 27312, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPDATE
          Blocked by inst: 1, sid: 810, ser: 24481
          Final Blocker inst: 1, sid: 810, ser: 24481

      66: J00A ospid 620994 sid 561 ser 35509, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPDATE
          Blocked by inst: 1, sid: 810, ser: 24481
          Final Blocker inst: 1, sid: 810, ser: 24481
      68: M002 ospid 618975 sid 22 ser 30742, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPDATE
          Blocked by inst: 1, sid: 810, ser: 24481
          Final Blocker inst: 1, sid: 810, ser: 24481
      69: MZ00 ospid 620998 sid 295 ser 41723, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPSERT
          Blocked by inst: 1, sid: 810, ser: 24481
          Final Blocker inst: 1, sid: 810, ser: 24481
      70: M004 ospid 618953 sid 557 ser 23163, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPSERT
          Blocked by inst: 1, sid: 810, ser: 24481
          Final Blocker inst: 1, sid: 810, ser: 24481
In another test, we got following DB alert.log:

2025-08-25T10:50:32.827370+02:00
DIA0 Critical Database Process As Root: Hang ID 1 blocks 11 sessions
     Final blocker is session ID 810 serial# 31793 OSPID 601523 on Instance 1
     If resolvable, instance eviction will be attempted by Hang Manager
2025-08-25T10:50:37.839966+02:00
System State dumped to trace file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_dia0_601521_base_1.trc
2025-08-25T10:50:39.601999+02:00
Errors in file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_dia0_601521_base_1.trc:
ORA-32515: cannot issue ORADEBUG command 'SHORT_STACK' to process 'Unix process pid: 601523, image: oracle@test_db3 (DBW0)'; prior command execution time exceeds 5000 ms
2025-08-25T10:50:44.283104+02:00
DIA0 Critical Database Process As Root: Hang ID 2 blocks 3 sessions
     Final blocker is session ID 811 serial# 20147 OSPID 601539 on Instance 1
     If resolvable, instance eviction will be attempted by Hang Manager
2025-08-25T10:52:10.789009+02:00
System state dump requested by (instance=1, osid=601531 (DBW2)), summary=[system state dump request (ksudss)].
2025-08-25T10:52:10.795339+02:00
Errors in file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_dbw2_601531.trc:
ORA-32515: cannot issue ORADEBUG command 'SHORT_STACK' to process 'Unix process pid: 601523, image: oracle@test_db3 (DBW0)'; prior command execution time exceeds 5000 ms
2025-08-25T10:52:10.805672+02:00
Errors in file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_dbw1_601527.trc:
ORA-32515: cannot issue ORADEBUG command 'SHORT_STACK' to process 'Unix process pid: 601523, image: oracle@test_db3 (DBW0)'; prior command execution time exceeds 5000 ms
System State dumped to trace file /orabin/app/oracle/admin/test_db3/diag/rdbms/test_db3/test_db3/trace/test_db3_diag_601504_20250825105210.trc
2025-08-25T10:52:11.546767+02:00
DBW 1 executing after 6 secs due to non-DBWR task at ts=1756111931
2025-08-25T10:52:11.646360+02:00
DBW 2 executing after 6 secs due to non-DBWR task at ts=1756111931
It shows "ORADEBUG command 'SHORT_STACK' execution time exceeds 5000 ms":

ORA-32515: cannot issue ORADEBUG command 'SHORT_STACK' to process 'Unix process pid: 601523, image: oracle@test_db3 (DBW0)'; prior command execution time exceeds 5000 ms
"5000 ms" seems hard-coded, if we look kjznlwshngsew (caller of sskgp_tgkill), we can see (5000 = 0x1388):

   0x0000000003357d4b <+2715>:	mov    $0x1388,%edx
   0x0000000003357d50 <+2720>:	callq  0x13570f0 
Open "test_db3_diag_601504_20250825105210.trc", the systemstate dump shows DB blocked by DBW0 (sid 810):

----------- test_db3_diag_601504_20250825105210.trc -----------

      19: DBW0 ospid 601523 sid 810 ser 31793, waiting for 'db file parallel write'
      20: DBW1 ospid 601527 sid 5 ser 51041,
      21: DBW2 ospid 601531 sid 273 ser 53811,
      22: LGWR ospid 601535 sid 542 ser 33370, waiting for 'rdbms ipc message'
      23: CKPT ospid 601539 sid 811 ser 20147, waiting for 'rdbms ipc message'

      34: MMON ospid 601569 sid 545 ser 42846, waiting for 'log file switch (checkpoint incomplete)'
          Blocked by inst: 1, sid: 810, ser: 31793
          Final Blocker inst: 1, sid: 810, ser: 31793

      38: USER ospid 601876 sid 554 ser 30768, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPDATE
          Blocked by inst: 1, sid: 810, ser: 31793
          Final Blocker inst: 1, sid: 810, ser: 31793
      
      47: W003 ospid 601811 sid 816 ser 37737, waiting for 'log file switch (checkpoint incomplete)'
          Blocked by inst: 1, sid: 810, ser: 31793
          Final Blocker inst: 1, sid: 810, ser: 31793

      57: USER ospid 601960 sid 290 ser 217, waiting for 'DLM cross inst call completion'
          Cmd: PL/SQL EXECUTE
          Blocked by inst: 1, sid: 811, ser: 20147
          Final Blocker inst: 1, sid: 811, ser: 20147
      58: M002 ospid 601840 sid 555 ser 44546, waiting for 'class slave wait'
      59: J002 ospid 601942 sid 824 ser 55525, waiting for 'log file switch (checkpoint incomplete)'
          Cmd: UPDATE
          Blocked by inst: 1, sid: 810, ser: 31793
          Final Blocker inst: 1, sid: 810, ser: 31793
      60: J000 ospid 601938 sid 20 ser 28041, waiting for 'enq: US - contention'
          Cmd: UPDATE
          Blocked by inst: 1, sid: 826, ser: 1291
          Final Blocker inst: 1, sid: 810, ser: 31793


2.7 Session 'oradebug request completion' Stats



select localtimestamp, event, total_waits, total_timeouts, round(time_waited_micro/total_waits) ms_per_wait, round(time_waited_micro/1e6) wait_seconds --,  v.* 
from v$system_event v where lower(event) = 'oradebug request completion';

LOCALTIMESTAMP         EVENT                        TOTAL_WAITS TOTAL_TIMEOUTS MS_PER_WAIT WAIT_SECONDS
---------------------- ---------------------------- ----------- -------------- ----------- ------------
27-AUG-2025 12:14:45   oradebug request completion         2507             67     1006642         2524

select sample_time, program, session_id, session_serial#, event 
from v$active_session_history t 
where sample_time between timestamp'2025-8-26 20:38:40' and timestamp'2025-8-26 20:40:35'
  and event in ('oradebug request completion')
order by t.sample_time;

SAMPLE_TIME             PROGRAM                  SESSION_ID SESSION_SERIAL# EVENT
----------------------- ------------------------ ---------- --------------- ---------------------------
26-AUG-2025 20:38:40    oracle@test_db3 (DIA0)          541           16758 oradebug request completion
26-AUG-2025 20:40:19    oracle@test_db3 (DBW1)            5            1428 oradebug request completion
26-AUG-2025 20:40:20    oracle@test_db3 (DBW1)            5            1428 oradebug request completion
26-AUG-2025 20:40:21    oracle@test_db3 (DBW1)            5            1428 oradebug request completion
26-AUG-2025 20:40:22    oracle@test_db3 (DBW1)            5            1428 oradebug request completion
26-AUG-2025 20:40:23    oracle@test_db3 (DBW1)            5            1428 oradebug request completion
26-AUG-2025 20:40:24    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:25    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:26    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:27    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:28    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:29    oracle@test_db3 (DBW2)          273           22218 oradebug request completion
26-AUG-2025 20:40:29    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:30    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:30    oracle@test_db3 (DBW2)          273           22218 oradebug request completion
26-AUG-2025 20:40:31    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:31    oracle@test_db3 (DBW2)          273           22218 oradebug request completion
26-AUG-2025 20:40:32    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:32    oracle@test_db3 (DBW2)          273           22218 oradebug request completion
26-AUG-2025 20:40:33    oracle@test_db3 (DBW2)          273           22218 oradebug request completion
26-AUG-2025 20:40:33    oracle@test_db3 (DIAG)          808           30814 oradebug request completion
26-AUG-2025 20:40:34    oracle@test_db3 (DIAG)          808           30814 oradebug request completion


2.8 Session Process Info


Here the session info ( SID, SERIAL#, PID, SPID) in our test:

select s.program, event, s.sid, s.serial#, pid, spid  
  from v$session s, v$process p 
 where s.paddr=p.addr and (lower(s.program) like '%(dia%' or lower(s.program) like '%(dbw%' 
       or lower(s.program) like '%(ckpt%' or lower(s.program) like '%(lg%' or lower(s.program) like '%(j0%');

PROGRAM                   EVENT                        SID  SERIAL#   PID SPID
------------------------- -------------------------- ----- -------- ----- ------
oracle@test_db3 (DIAG)    DIAG idle wait               808    30814    11 604074
oracle@test_db3 (DIA0)    DIAG idle wait               541    16758    18 604091
oracle@test_db3 (DBW0)    db file parallel write       810    24481    19 604093
oracle@test_db3 (DBW1)    db file parallel write         5     1428    20 604097
oracle@test_db3 (DBW2)    db file parallel write       273    22218    21 604101
oracle@test_db3 (LGWR)    rdbms ipc message            542     2191    22 604105
oracle@test_db3 (CKPT)    rdbms ipc message            811     4896    23 604109
oracle@test_db3 (LG00)    LGWR worker group idle         6     7534    24 604113
oracle@test_db3 (LG01)    LGWR worker group idle       543    61931    26 604119
oracle@test_db3 (J000)    db file sequential read       21    42342    56 620865
oracle@test_db3 (J001)    free buffer waits            292     4060    57 620867
oracle@test_db3 (J002)    db file sequential read      560    27312    58 620869
oracle@test_db3 (J003)    free buffer waits            829    13992    59 620871
oracle@test_db3 (J004)    db file sequential read       20    48258    60 620873
oracle@test_db3 (J005)    db file sequential read      289     7069    61 620875
oracle@test_db3 (J006)    db file sequential read      562    16826    62 620877
oracle@test_db3 (J007)    db file sequential read      830    19629    63 620879
oracle@test_db3 (J008)    db file sequential read        2    61229    64 620881
oracle@test_db3 (J009)    db file sequential read      291     8814    65 620883
oracle@test_db3 (J00A)    jobq slave wait              561    24036    66 620885


3. Observing "oradebug request completion"


With following small test, we can watch "oradebug request completion". Open two Sqlplus sessions (PID 36 and 59):

select s.last_call_et, s.program, event, s.sid, s.serial#, pid, spid  
  from v$session s, v$process p 
 where s.paddr=p.addr and lower(s.program) like '%sqlplus%' order by 1;
 
LAST_CALL_ET PROGRAM                        EVENT                         SID  SERIAL#  PID SPID
------------ ------------------------------ ---------------------------- ---- -------- ---- ------
          55 sqlplus.exe                    SQL*Net message from client    19    50486   36 613512
         639 sqlplus@test_db3 (TNS V1-V3)   SQL*Net message from client   829    46358   59 623458
Start GDB to catch signal SIGUSR2 (or "break sigprocmask") on PID: 36 (SPID 613512)

$> gdb -p 613512

(gdb) catch signal SIGUSR2
Catchpoint 1 (signal SIGUSR2)
(gdb) handle SIGUSR2
Signal        Stop      Print   Pass to program Description
SIGUSR2       Yes       Yes     Yes             User defined signal 2
(gdb) c
From PID 59, we run "oradebug short_stack" on PID 36:
  
SQL> oradebug setorapid 36
Oracle pid: 36, Unix process pid: 613512, image: oracle@test_db3
SQL> oradebug short_stack
GDB shows PID: 36 Catchpoint 1 (signal SIGUSR2) reached:

(gdb) catch signal SIGUSR2
Catchpoint 1 (signal SIGUSR2)
(gdb) handle SIGUSR2
Signal        Stop      Print   Pass to program Description
SIGUSR2       Yes       Yes     Yes             User defined signal 2
(gdb) c
Continuing.

Catchpoint 1 (signal SIGUSR2), 0x00007f53174d79b2 in read () from /lib64/libpthread.so.0
(gdb) bt
#0  0x00007f53174d79b2 in read () from /lib64/libpthread.so.0
#1  0x0000000013a74cb4 in read ()
#2  0x0000000013a70950 in snttread ()
#3  0x00000000065a25f5 in nttrd ()
#4  0x00000000064afe40 in nsprecv ()
#5  0x00000000064b8632 in nsrdr ()
#6  0x0000000012bcf1d5 in nsfull_pkt_rcv ()
#7  0x0000000012bd688e in nsfull_brc ()
#8  0x0000000013a5f886 in nioqrc ()
#9  0x00000000135c893a in opikndf2 ()
#10 0x00000000029739c2 in opitsk ()
#11 0x0000000002979ccb in opiino ()
#12 0x00000000135cb7a0 in opiodr ()
#13 0x000000000296fa86 in opidrv ()
#14 0x00000000035da375 in sou2o ()
#15 0x0000000000e0b566 in opimai_real ()
#16 0x00000000035e7011 in ssthrdmain ()
#17 0x0000000000e0b390 in main ()
Now if we look Session (PID 59), it is waiting on "oradebug request completion" because it sends oradebug request on PID 36,
which is pending to continuation, hence PID 59 waiting for completion.

select s.last_call_et, s.program, event, s.sid, s.serial#, pid, spid  
  from v$session s, v$process p 
 where s.paddr=p.addr and lower(s.program) like '%sqlplus%' order by 1;

LAST_CALL_ET PROGRAM                        EVENT                           SID   SERIAL#  PID SPID
------------ ------------------------------ ---------------------------- ------ --------- ---- ------
         191 sqlplus.exe                    SQL*Net message from client      19     50486   36 613512
         775 sqlplus@test_db3 (TNS V1-V3)   oradebug request completion     829     46358   59 623458
By the way, we also observe "oradebug request completion" in Oracle 19.9.


4. strace on DBW0


With strace, we can watch asynchronous I/O (io_submit and io_getevents):

$> strace -t -p 604093

11:29:40 io_submit(140598410809344, 5, 
            [{data=0x7fdf9f22a738, pwrite, fildes=261, str="\2\242\0\0Z\2\300\0\202\377\216\0355\f\1\4\334\225\0\0\f\0\33\0| \7\0\35E\16\16"..., nbytes=40960, offset=4931584}, 
             {data=0x7fdf9ef17108, pwrite, fildes=261, str="\2\242\0\0qt\301\0\210\377\216\0355\f\2\4I\335\0\0\n\0!\0M\306\n\0`\343\2\2"..., nbytes=40960, offset=781066240}, 
             {data=0x7fdf9eed1758, pwrite, fildes=261, str="\2\242\0\0)\340\323\0J\377\216\0355\f\6\4^\\\0\0\4\0\35\0\210n\10\0\225\210\10\10"..., nbytes=8192, offset=10670645248}, 
             {data=0x7fdf9ee2ae38, pwrite, fildes=261, str="\2\242\0\0I\273\336\0k\377\216\0355\f\1\4\254|\0\0\t\0\27\0\37\373\7\0\272\204\2\2"..., nbytes=8192, offset=16498892800}, 
             {data=0x7fdf9eea4658, pwrite, fildes=2865, str="\6\242\0\0\221\0@\214\227\377\216\0355\f\2\6o$\0\0\1\0\0\0\236\234Z\0\226\377\216\35"..., nbytes=32768, offset=1187840}]) = 5
11:29:40 io_getevents(140598410809344, 1, 128, 
            [{data=0x7fdf9ee2ae38, obj=0x7fdf9ee2ae38, res=8192, res2=0}, 
             {data=0x7fdf9eed1758, obj=0x7fdf9eed1758, res=8192, res2=0}, 
             {data=0x7fdf9ef17108, obj=0x7fdf9ef17108, res=40960, res2=0}, 
             {data=0x7fdf9f22a738, obj=0x7fdf9f22a738, res=40960, res2=0}], 
             {600, 0}) = 4
11:29:40 io_getevents(140598410809344, 1, 128, 
            [{data=0x7fdf9eea4658, obj=0x7fdf9eea4658, res=32768, res2=0}], 
             {600, 0}) = 1
          
$ > ./lsof -p 604093

  ora_dbw0_ 604093 oracle  261uW  REG     8,1 34359730176  688992556 /oratest_db3/oradata/test_db3/undo01.dbf
  ora_dbw0_ 604093 oracle 2865uW  REG   253,2     5251072         12 /home/oracle/suk/slow_dir3/test_tbs_slow3.dbf
Above io_submit sends 5 pwrite requests (4 on UNOD file (fildes=261), one on tablespace file (fildes=2865) of table test_tab_slow3),
and calls twice io_getevents to read asynchronous I/O events.
(io_submit and io_getevents are matched by "data" and "res" fields. Their order can be different due to asynchronous)

With asynchronous I/O, DBWR calls io_submit to queue I/O requests, kworkers handle actual I/O requests, once I/O operation finishes,
DBWR is signalled and read asynchronous I/O events from the completion queue by io_getevents.
Hence DBWR seems idle even there are heavy I/O activities.

Often "oradebug request completion" is cause by unusually high storage latency or hardware issues, indicating a performance bottleneck.
In AWR reports, we can see those outliers (Av Rd/ Av Writes) of "db file async I/O submit", "db file parallel write" and IOStat sections
(by function, filetype, tablespace,file).

If Linux AIO-MAX-NR is set too (AIO-MAX-NR=131072 AIO-NR=13104), following WARNING wrote into DB Writer trc and DB Performance deteriorated sluggishly. (If currently active AIO number: aio-nr reaches aio-max-nr then io_setup will fail with EAGAIN):

WARNING:1 Oracle process running out of OS kernelI/O resources request_aiolimit=4096 set_aiolimit=0
 
WARNING:asynch I/O kernel limits is set at AIO-MAX-NR=131072 AIO-NR=13104
We can also monitor them by querying v$io_outlier:

select function_name, file_name, disk1_name, count(*) cnt, sum(io_latency) io_latency_SUM, round(avg(io_latency)) io_latency_AVG
  from v$io_outlier where disk1_name = 'dm-3' group by function_name, file_name, disk1_name order by file_name, disk1_name;

FUNCTION_NAME        FILE_NAME                                          DISK1_NAME        CNT IO_LATENCY_SUM IO_LATENCY_AVG
-------------------- -------------------------------------------------- ---------- ---------- -------------- --------------
Buffer Cache Reads   /home/oracle/suk/slow_dir3/test_tbs_slow3.dbf      dm-3              156         816345           5233
DBWR                 /home/oracle/suk/slow_dir3/test_tbs_slow3.dbf      dm-3               94        1127662          11996
Direct Reads         /home/oracle/suk/slow_dir3/test_tbs_slow3.dbf      dm-3                4          25645           6411
Direct Writes        /home/oracle/suk/slow_dir3/test_tbs_slow3.dbf      dm-3                1          10239          10239
Others               /home/oracle/suk/slow_dir3/test_tbs_slow3.dbf      dm-3                4          21308           5327
We can get full aio_write call stack by:

$> perf record -e 'workqueue:*' -ag -T

$> perf report

-- Type "Enter" on "19K workqueue:workqueue_queue_work", type "/ Filter symbol by name", then give "io_submit_one"

Samples: 19K of event 'workqueue:workqueue_queue_work', Event count (approx.): 19945
  Children      Self  Command          Shared Object      Symbol
-    5.85%     0.00%  ora_dbw0_test_d  [kernel.kallsyms]  [k] io_submit_one                           
   - io_submit_one                                                                                    
      - 5.77% aio_write                                                                               
         - 5.64% xfs_file_write_iter                                                                  
            - 5.63% xfs_file_dio_write_aligned                                                        
               - 5.37% iomap_dio_rw                                                                   
                  - __iomap_dio_rw                                                                    
                     - 5.07% iomap_apply                                                              
                        - 4.91% iomap_dio_bio_actor                                                   
                           - 4.37% iomap_dio_submit_bio.isra.23                                       
                              - 4.33% submit_bio                                                      
                                 - generic_make_request                                               
                                    - 4.16% blk_mq_make_request                                       
                                       - 2.76% blk_flush_plug_list                                    
                                          - blk_mq_flush_plug_list                                    
                                             - 2.76% blk_mq_sched_insert_requests                     
                                                - 2.70% __blk_mq_delay_run_hw_queue                   
                                                   - 2.70% __blk_mq_run_hw_queue                      
                                                        blk_mq_sched_dispatch_requests                
                                                        __blk_mq_sched_dispatch_requests              
                                                      - __blk_mq_do_dispatch_sched                    
                                                         - 2.57% blk_mq_dispatch_rq_list              
                                                            - 2.56% scsi_queue_rq                     
                                                               - 2.32% pvscsi_queue                   
                                                                    _raw_spin_unlock_irqrestore       
                                                                    ret_from_intr                     
                                                                    do_IRQ                            
                                                                    irq_exit                          
                                                                    irq_exit_rcu                      
                                                                    __softirqentry_text_start         
                                                                    blk_complete_reqs                 
                                                                    scsi_io_completion                
                                                                  - scsi_end_request                  
                                                                     - 2.31% blk_update_request       
                                                                          iomap_dio_bio_end_io        
                                                                          queue_work_on               
                                                                          __queue_work                
                                                                          __queue_work                
                                       - 1.11% __blk_mq_alloc_request                                 
                                          - 1.11% blk_mq_get_tag                                      
                                             - 1.07% io_schedule                                      
                                                  io_schedule_prepare                                 
                                                  blk_flush_plug_list                                 
                                                  blk_mq_flush_plug_list                              
                                                  blk_mq_sched_insert_requests                        
                                                  kblockd_mod_delayed_work_on                         
                                                - mod_delayed_work_on                                 
                                                   - 1.07% __queue_work                               
                                                        __queue_work                                  
With following bpftrace script, we can also get similar info as strace:

bpftrace -e 'tracepoint:syscalls:sys_enter_io_submit /pid==604093/ {
              $iocb_sizeof = sizeof(struct iocb);
              $reqs = args->nr;
              $iocbpp_ptr = (struct iocb **)args->iocbpp;
              time("%H:%M:%S --- ");
              printf("%s, iocb_sizeof: %d, io_submit I/O requests: %d\n", comm, $iocb_sizeof, $reqs);
              $i = 1; while ($i <= $reqs) { $iocbpp_ptr_2 = (struct iocb **)($iocbpp_ptr + $iocb_sizeof*($i-1));
                                            $iocbpp_struct = (struct iocb *)(*$iocbpp_ptr_2);
                                            printf("--- File: %d --- ptr_2 %p, aio_fildes: %d, aio_nbytes: %d, aio_offset: %ld, aio_lio_opcode(1=PWRITE): %d, aio_data:0X%X\n",
                                                   $i, $iocbpp_ptr_2, $iocbpp_struct->aio_fildes, $iocbpp_struct->aio_nbytes, $iocbpp_struct->aio_offset, $iocbpp_struct->aio_lio_opcode, $iocbpp_struct->aio_data);
                                            @All_CNT = count(); @CNT[$iocbpp_struct->aio_fildes] = count(); @SUM[$iocbpp_struct->aio_fildes] = sum( $iocbpp_struct->aio_nbytes);
                                            if ($i > 50) { break; }
                                            $i++ }
              printf("\n");}
              tracepoint:syscalls:sys_exit_io_getevents /pid==604093/ {time("%H:%M:%S --- "); printf("-- 1 -- io_getevents Ret: %d\n", args->ret); @Ret_CNT = sum(args->ret);
              }'


5. ORA-32515 - ORA-32519


Above DB alert.log shows ORA-32515. Here some ORADEBUG related errors:

oracle@test_db3:test_db3 > oerr ora 32515
32515, 00000, "cannot issue ORADEBUG command '%s' to process '%s'; prior command execution time exceeds %s ms"
// *Cause:  The process targeted to execute the ORADEBUG command was
//          busy executing another ORADEBUG command for a time greater
//          than the timeout value specified.
// *Action: Increase the timeout value.

oracle@test_db3:test_db3 > oerr ora 32516
32516, 00000, "cannot wait for process '%s' to finish executing ORADEBUG command '%s'; wait time exceeds %s ms"
// *Cause:  The execution of the ORADEBUG command took longer than the
//          the timeout value specified.
// *Action: Increase the timeout value.

oracle@test_db3:test_db3 > oerr ora 32517
32517, 00000, "cannot issue ORADEBUG command '%s' to process '%s' (prior command execution time exceeds %s ms);  total wait time exceeds %s ms"
// *Cause:  The process targeted to execute the ORADEBUG command was
//          busy executing another ORADEBUG command. But, because
//          the total wait time for all targeted processes
//          exceeded the maximum wait time, the ORADEBUG command was
//          not issued to the target process.
// *Action: Increase the timeout value.

oracle@test_db3:test_db3 > oerr ora 32518
32518, 00000, "cannot wait for process '%s' to finish executing ORADEBUG command '%s' (waited %s ms); total wait time exceeds %s ms"
// *Cause:  The total wait time for all targeted processes exceeded the
//          maximum wait time, therefore the wait for the targeted process
//          to finish executing the ORADEBUG command was aborted.
// *Action: Increase the timeout value.

oracle@test_db3:test_db3 > oerr ora 32519
32519, 00000, "insufficient privileges to execute ORADEBUG command: %s"
// *Cause: Sufficient privileges were not granted.
// *Action: Perform the operation using a database session that
//          has the required privileges. See the Oracle ORADEBUG
//          documentation for more details.
ORADEBUG probably has some changes, for example,
Oracle 19.19 Patch:
     Patch 25293381: RUNNING "ORADEBUG SETTRACEFILEID TEST" CAUSES ORA-32522: [KGHALO BAD HEAP DS]

And we also noticed that from Oracle 19.26, DBW0 generates a trc file of more than 1 MB.

Sunday, August 3, 2025

Reading CLOB Line by Line with SQL and Plsql: ChatGPT and Gemini Implementations

Following previous Blog: Reading CLOB Line by Line with SQL and Plsql,
we will show and test SQL and Plsql implementations provided by ChatGPT and Gemini.

Note: Tested on Oracle 19c.


1. ChatGPT


1.1 ChatGPT SQL Implementation


Ask: 
  SQL-Only: Read Oracle CLOB Line by Line

Answer: 

WITH clob_data AS (
    SELECT your_clob_column AS clob_text
    FROM your_table
    WHERE your_condition
),
lines AS (
    SELECT LEVEL AS line_num,
           TRIM(REGEXP_SUBSTR(clob_text, '[^(\r\n)]+', 1, LEVEL)) AS line_text
    FROM clob_data
    CONNECT BY REGEXP_SUBSTR(clob_text, '[^(\r\n)]+', 1, LEVEL) IS NOT NULL
)
SELECT line_num, line_text
FROM lines;

----------------------- Test Procedure: Reading 1,000 Lines takes 3 Minutes -----------------------

create or replace procedure SQL_ChatGPT(p_id number) is
  l_row_cnt      number := power(10, p_id);  -- requires rownum limit, otherwise endless loop
  l_cnt          number := 0;
  l_len          number := 0;
begin
  for c in (
with clob_data as (select mylob clob_text from test_tab_lob where id = p_id),
lines AS (
    SELECT LEVEL AS line_num,
           TRIM(REGEXP_SUBSTR(clob_text, '[^' || chr(10) || ']+', 1, LEVEL)) AS line_text
    FROM clob_data
    CONNECT BY REGEXP_SUBSTR(clob_text,  '[^' || chr(10) || ']+', 1, LEVEL) IS NOT NULL and rownum <=l_row_cnt
)
SELECT line_num, line_text
FROM lines
  ) loop
    l_cnt := l_cnt + 1;
    l_len := l_len + length(c.line_text);
  end loop;
  dbms_output.put_line('CNT = '||l_cnt ||', Length = '||l_len);
end;
/


SQL > exec SQL_ChatGPT(3);
  CNT = 1,000, Length = 509,455
  Elapsed: 00:03:12.20
The performance is the similar to SQL_SLOW in previous Blog, Quadratic Performance (O(n^2)) and heavy "direct path read".
In the original code, rownum limit condition has to be added, otherwise endless loop.


1.2 ChatGPT Plsql Implementation


Ask:
  PL/SQL code to read a CLOB line by line

Answer:

DECLARE
  v_clob       CLOB;
  v_line       VARCHAR2(32767);
  v_pos        INTEGER := 1;
  v_newline    INTEGER;
  v_clob_len   INTEGER;
BEGIN
  -- Get your CLOB (example: first row from your_table)
  SELECT your_clob_column INTO v_clob FROM your_table WHERE rownum = 1;

  v_clob_len := DBMS_LOB.getlength(v_clob);

  WHILE v_pos <= v_clob_len LOOP
    -- Find next newline position
    v_newline := DBMS_LOB.INSTR(v_clob, CHR(10), v_pos);

    IF v_newline = 0 THEN
      -- No more newlines: read till end of CLOB
      v_line := DBMS_LOL.SUBSTR(v_clob, v_clob_len - v_pos + 1, v_pos);
      DBMS_OUTPUT.PUT_LINE(v_line);
      EXIT;
    ELSE
      -- Read from current pos up to newline -1 (excluding newline)
      v_line := DBMS_LOB.SUBSTR(v_clob, v_newline - v_pos, v_pos);
      DBMS_OUTPUT.PUT_LINE(v_line);

      -- Move position to character after newline
      v_pos := v_newline + 1;
    END IF;
  END LOOP;
END;
Note: above code contains: "DBMS_LOL", it is not clear how it comes from.

----------------------- Test Procedure: Reading 100,000 Lines takes 47 Seconds -----------------------

create or replace procedure PLSQL_ChatGPT(p_id number) is
--DECLARE
  v_clob       CLOB;
  v_line       VARCHAR2(32767);
  v_pos        INTEGER := 1;
  v_newline    INTEGER;
  v_clob_len   INTEGER;
  l_cnt          number := 0;
  l_len          number := 0;
BEGIN
  -- Get your CLOB (example: first row from your_table)
  --SELECT your_clob_column INTO v_clob FROM your_table WHERE rownum = 1;
  select mylob into v_clob from test_tab_lob where id = p_id;

  v_clob_len := DBMS_LOB.getlength(v_clob);

  WHILE v_pos <= v_clob_len LOOP
    -- Find next newline position
    v_newline := DBMS_LOB.INSTR(v_clob, CHR(10), v_pos);

    IF v_newline = 0 THEN
      -- No more newlines: read till end of CLOB
      -- v_line := DBMS_LOL.SUBSTR(v_clob, v_clob_len - v_pos + 1, v_pos);    
      v_line := DBMS_LOB.SUBSTR(v_clob, v_clob_len - v_pos + 1, v_pos);
      DBMS_OUTPUT.PUT_LINE(v_line);
      EXIT;
    ELSE
      -- Read from current pos up to newline -1 (excluding newline)
      v_line := DBMS_LOB.SUBSTR(v_clob, v_newline - v_pos, v_pos);
      --DBMS_OUTPUT.PUT_LINE(v_line);

      -- Move position to character after newline
      v_pos := v_newline + 1;
    END IF;
    
    l_cnt := l_cnt + 1;
    l_len := l_len + length(v_line);
  END LOOP;
  dbms_output.put_line('CNT = '||l_cnt ||', Length = '||l_len);
END;
/

SQL > exec PLSQL_ChatGPT(5);
  CNT = 100,000, Length = 99,509,455
  Elapsed: 00:00:47.04


2. Gemini


2.1 Gemini SQL Implementation


Ask: 
  read oracle clob line by line with pure recursive SQL

Answer: 

WITH
  clob_data AS (
    -- Replace this CTE with your actual table and CLOB column
    SELECT
      TO_CLOB(
        'This is the first line of the CLOB.' || CHR(10) ||
        'Second line is here.' || CHR(10) ||
        'And the very last line, third one.'
      ) AS my_clob
    FROM
      dual
  ),
  lines_recursive (line_num, current_pos, remaining_clob, line_text) AS (
    -- Anchor Member: Start with the first line
    SELECT
      1 AS line_num,
      1 AS current_pos,
      c.my_clob AS remaining_clob,
      CASE
        WHEN INSTR(c.my_clob, CHR(10), 1) > 0 THEN SUBSTR(c.my_clob, 1, INSTR(c.my_clob, CHR(10), 1) - 1)
        ELSE c.my_clob
      END AS line_text
    FROM
      clob_data c
    UNION ALL
    -- Recursive Member: Process the next line
    SELECT
      lr.line_num + 1 AS line_num,
      INSTR(lr.remaining_clob, CHR(10), 1) + 1 AS current_pos,
      SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1) AS remaining_clob,
      CASE
        WHEN INSTR(SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1), CHR(10), 1) > 0 
             THEN SUBSTR(SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1), 1, 
                INSTR(SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1), CHR(10), 1) - 1)
        ELSE SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1)
      END AS line_text
    FROM
      lines_recursive lr
    WHERE
      INSTR(lr.remaining_clob, CHR(10), 1) > 0 -- Continue as long as there are more newlines
  )
SELECT
  line_num,
  line_text
FROM
  lines_recursive;
First time when asking "read oracle clob line by line with SQL only", Gemini replied:
     I'm sorry, but it is not possible to read an Oracle CLOB line by line using SQL only.'

----- Test Procedure: Reading 10,000 Lines hit "ORA-01652: unable to extend temp segment by  in tablespace" ------

create or replace procedure SQL_Gemini(p_id number) is
  l_cnt          number := 0;
  l_len          number := 0;
begin
  for c in (
with clob_data as (select mylob my_clob from test_tab_lob where id = p_id),
  lines_recursive (line_num, current_pos, remaining_clob, line_text) AS (
    -- Anchor Member: Start with the first line
    SELECT
      1 AS line_num,
      1 AS current_pos,
      c.my_clob AS remaining_clob,
      CASE
        WHEN INSTR(c.my_clob, CHR(10), 1) > 0 THEN SUBSTR(c.my_clob, 1, INSTR(c.my_clob, CHR(10), 1) - 1)
        ELSE c.my_clob
      END AS line_text
    FROM
      clob_data c
    UNION ALL
    -- Recursive Member: Process the next line
    SELECT
      lr.line_num + 1 AS line_num,
      INSTR(lr.remaining_clob, CHR(10), 1) + 1 AS current_pos,
      SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1) AS remaining_clob,
      CASE
        WHEN INSTR(SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1), CHR(10), 1) > 0 
             THEN SUBSTR(SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1), 1, 
                INSTR(SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1), CHR(10), 1) - 1)
        ELSE SUBSTR(lr.remaining_clob, INSTR(lr.remaining_clob, CHR(10), 1) + 1)
      END AS line_text
    FROM
      lines_recursive lr
    WHERE
      INSTR(lr.remaining_clob, CHR(10), 1) > 0 -- Continue as long as there are more newlines
  )
SELECT
  line_num,
  line_text
FROM
  lines_recursive
  ) loop
    l_cnt := l_cnt + 1;
    l_len := l_len + length(c.line_text);
  end loop;
  dbms_output.put_line('CNT = '||l_cnt ||', Length = '||l_len);
end;
/

SQL > exec SQL_Gemini(4);
  BEGIN SQL_Gemini(4); END;
  *
  ERROR at line 1:
  ORA-01652: unable to extend temp segment by  in tablespace
  ORA-06512: at "K.SQL_GEMINI", line 6
  ORA-06512: at "K.SQL_GEMINI", line 6
  ORA-06512: at line 1
  Elapsed: 00:01:38.98

-- v$active_session_history.temp_space_allocated shows about 8 GB.


2.2 Gemini Plsql Implementation


Ask:
  read oracle clob line by line with PLSQL only

Answer:

DECLARE
    l_clob              CLOB;
    l_offset            NUMBER := 1;
    l_line_start        NUMBER := 1;
    l_line_end          NUMBER;
    l_line_buffer       VARCHAR2(32767); -- Max PL/SQL VARCHAR2 size
    l_newline_char_unix VARCHAR2(1) := CHR(10);
    l_newline_char_win  VARCHAR2(2) := CHR(13) || CHR(10);
    l_newline_length    NUMBER;
    l_clob_length       NUMBER;

BEGIN
    -- 1. Populate a CLOB for demonstration (replace with your actual CLOB source)
    l_clob := 'This is the first line.' || CHR(10) ||
              'This is the second line with some more text.' || CHR(10) ||
              'And a third line.' || CHR(13) || CHR(10) || -- Example with Windows newline
              'Fourth line after Windows newline.' || CHR(10) ||
              'Last line without a trailing newline.';

    l_clob_length := DBMS_LOB.GETLENGTH(l_clob);

    -- Loop through the CLOB
    WHILE l_offset <= l_clob_length LOOP
        -- Try to find Unix-style newline first
        l_line_end := INSTR(l_clob, l_newline_char_unix, l_offset);
        l_newline_length := 1; -- Length of Unix newline

        -- If not found or if a Windows newline is found earlier
        IF l_line_end = 0 OR (INSTR(l_clob, l_newline_char_win, l_offset) > 0 AND INSTR(l_clob, l_newline_char_win, l_offset) < l_line_end) THEN
            l_line_end := INSTR(l_clob, l_newline_char_win, l_offset);
            l_newline_length := 2; -- Length of Windows newline
        END IF;

        IF l_line_end = 0 THEN
            -- No more newlines found, this is the last "line" (or the only line)
            l_line_buffer := DBMS_LOB.SUBSTR(l_clob, l_clob_length - l_offset + 1, l_offset);
            l_offset := l_clob_length + 1; -- Exit loop
        ELSE
            -- Newline found, extract the line
            l_line_buffer := DBMS_LOB.SUBSTR(l_clob, l_line_end - l_offset, l_offset);
            l_offset := l_line_end + l_newline_length;
        END IF;

        -- Process the extracted line
        DBMS_OUTPUT.PUT_LINE('Line: "' || l_line_buffer || '"');
    END LOOP;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/

----------------------- Test Procedure: Reading 100,000 Lines takes 44 Seconds -----------------------
-- Remove l_newline_char_win check, only check l_newline_char_unix
-- If check both l_newline_char_win and l_newline_char_unix, it takes 

create or replace procedure PLSQL_Gemini(p_id number, p_check_both number:= 0) is
    l_clob              CLOB;
    l_offset            NUMBER := 1;
    l_line_start        NUMBER := 1;
    l_line_end          NUMBER;
    l_line_buffer       VARCHAR2(32767); -- Max PL/SQL VARCHAR2 size
    l_newline_char_unix VARCHAR2(1) := CHR(10);
    l_newline_char_win  VARCHAR2(2) := CHR(13) || CHR(10);
    l_newline_length    NUMBER;
    l_clob_length       NUMBER;
    l_cnt               number := 0;
    l_len               number := 0;
BEGIN
    -- 1. Populate a CLOB for demonstration (replace with your actual CLOB source)
    select mylob into l_clob from test_tab_lob where id = p_id;
    l_clob_length := DBMS_LOB.GETLENGTH(l_clob);

    -- Loop through the CLOB
    WHILE l_offset <= l_clob_length LOOP
        -- Try to find Unix-style newline first
        l_line_end := INSTR(l_clob, l_newline_char_unix, l_offset);
        l_newline_length := 1; -- Length of Unix newline

        -- If not found or if a Windows newline is found earlier
        if p_check_both > 0 then 
          IF l_line_end = 0 OR (INSTR(l_clob, l_newline_char_win, l_offset) > 0 AND INSTR(l_clob, l_newline_char_win, l_offset) < l_line_end) THEN
              l_line_end := INSTR(l_clob, l_newline_char_win, l_offset);
              l_newline_length := 2; -- Length of Windows newline
          END IF;
        end if;

        IF l_line_end = 0 THEN
            -- No more newlines found, this is the last "line" (or the only line)
            l_line_buffer := DBMS_LOB.SUBSTR(l_clob, l_clob_length - l_offset + 1, l_offset);
            l_offset := l_clob_length + 1; -- Exit loop
        ELSE
            -- Newline found, extract the line
            l_line_buffer := DBMS_LOB.SUBSTR(l_clob, l_line_end - l_offset, l_offset);
            l_offset := l_line_end + l_newline_length;
        END IF;

        -- Process the extracted line
        --DBMS_OUTPUT.PUT_LINE('Line: "' || l_line_buffer || '"');
        l_cnt := l_cnt + 1;
        l_len := l_len + length(l_line_buffer);
    END LOOP;
    dbms_output.put_line('CNT = '||l_cnt ||', Length = '||l_len);
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/

-- Remove l_newline_char_win check, only check l_newline_char_unix

SQL > exec PLSQL_Gemini(5, 0);
  CNT = 100,000, Length = 99,509,455
  Elapsed: 00:00:44.32
  
-- Check both l_newline_char_win and l_newline_char_unix

SQL > exec PLSQL_Gemini(4, 1);  

$> perf top -d 2 -p 2464404  

   PerfTop:    2337 irqs/sec  kernel: 0.0%  exact:  0.0% lost: 0/0 drop: 0/0 [4000Hz cpu-clock:uhpppH],  (target_pid: 2464404)
-------------------------------------------------------------------------------------------------------------------------------------------

    42.87%  oracle         [.] kole_simple_string_match
    31.51%  oracle         [.] _intel_fast_memcmp
     2.50%  oracle         [.] kcbgtcr
     1.97%  libc-2.28.so   [.] syscall
     1.02%  oracle         [.] sxorchk
     0.86%  oracle         [.] __intel_avx_rep_memcpy


3. Test Outcome


3.1. ChatGPT SQL


SQL > exec SQL_ChatGPT(3);
  CNT = 1,000, Length = 509,455
  Elapsed: 00:03:04.59

SQL > exec SQL_ChatGPT(4);
  CNT = 10,000, Length = 9,509,455
  Elapsed: 08:20:01.13
 
SQL > exec SQL_ChatGPT(5);
 -- Not finished after 10 hours


3.2. ChatGPT PLSQL


SQL > exec PLSQL_ChatGPT(3);
  CNT = 1,000, Length = 509,455
  Elapsed: 00:00:00.38

SQL > exec PLSQL_ChatGPT(4);
  CNT = 10,000, Length = 9,509,455
  Elapsed: 00:00:04.04

SQL > exec PLSQL_ChatGPT(5);
  CNT = 100,000, Length = 99,509,455
  Elapsed: 00:00:36.66


3.3. Gemini SQL


SQL > exec SQL_Gemini(3);
  CNT = 1001, Length = 509455
  Elapsed: 00:00:05.60

SQL > exec SQL_Gemini(4);
  BEGIN SQL_Gemini(4); END;
  *
  ERROR at line 1:
  ORA-01652: unable to extend temp segment by  in tablespace
  ORA-06512: at "K.SQL_GEMINI", line 6
  ORA-06512: at "K.SQL_GEMINI", line 6
  ORA-06512: at line 1
  
  Elapsed: 00:01:35.50


3.4. Gemini PLSQL


SQL > exec PLSQL_Gemini(3);
  CNT = 1,000, Length = 509,455
  Elapsed: 00:00:00.35

SQL > exec PLSQL_Gemini(4);
  CNT = 10,000, Length = 9,509,455
  Elapsed: 00:00:04.28

SQL > exec PLSQL_Gemini(5);
  CNT = 100,000, Length = 99,509,455
  Elapsed: 00:00:38.75


3.5. Test Summary


(A). Both ChatGPT and Gemini provided efficient Plsql implementations.
(B). ChatGPT SQL implementation has the similar performance as SQL_SLOW in previous Blog (heavy "direct path read").
(C). Germin SQL implementation hits "ORA-01652: unable to extend temp segment by in tablespace" for 10,000 CLOB lines.
(D). Small adaptations/fixes could be needed to make them runnable.

Wednesday, July 30, 2025

Reading CLOB Line by Line with SQL and Plsql

This blog will demonstrate different implementations of reading CLOB line by line in SQL and Plsql.
For both SQL and Plsql, we show one slow and one fast version.

Then we run tests, compare and investigate their performance difference.

In next Blog: Reading CLOB Line by Line with SQL and Plsql: ChatGPT and Gemini Implementations
we will give a look of SQL and Plsql implementations provided by ChatGPT and Gemini.

Note: Tested on Oracle 19c.


1. Test Setup


1.1 Create Table and Filling Procedure


drop table test_tab_lob cascade constraints;

create table test_tab_lob(id number, mylob clob, note varchar2(100));

create or replace procedure create_test_data(p_id number, p_cnt number, p_last_line_no_LF number := 0) as 
  LF     constant varchar2(1)  := chr(10);
  l_mylob         clob; 
begin
  insert into test_tab_lob values (p_id, empty_clob(), 'CLOB Line Count = '||p_cnt||', Line_Last_Line_No_LF = '||p_last_line_no_LF) 
         returning mylob into l_mylob;
  
  for i in 1..p_cnt loop
      dbms_lob.append(dest_lob => l_mylob, src_lob => rpad('Line_'||i||'-', least(10+i-1, 1000), 'X')||LF);  
  end loop;
  if p_last_line_no_LF = 1 then 
    dbms_lob.append(dest_lob => l_mylob, src_lob => rpad('Line_Last_Line_No_LF-', 20, 'Y'));  -- add last line without LF
  end if;
  dbms_output.put_line(length(l_mylob));
  commit;
end;
/


1.2. Create Test Data


---- Smoking Test with LF at the end of Last Line
exec create_test_data(1, 5);

---- Smoking Test without LF at the end of Last Line
exec create_test_data(2, 5, 1);

---- CLOB with 1000 Lines
exec create_test_data(3, 1000);

---- CLOB with 10*1000 Lines
exec create_test_data(4, 10*1000);

---- CLOB with 100*1000 Lines
exec create_test_data(5, 100*1000);


1.3. Check Table CLOB Size


select id, length(mylob), note, round(dbms_lob.getlength(mylob)/1024/1024, 2) LOB_MB from test_tab_lob t;

    ID LENGTH(MYLOB) NOTE                                                             LOB_MB
  ---- ------------- ------------------------------------------------------------ ----------
     1            65 CLOB Line Count = 5, Line_Last_Line_No_LF = 0                         0
     2            85 CLOB Line Count = 5, Line_Last_Line_No_LF = 1                         0
     3        510455 CLOB Line Count = 1000, Line_Last_Line_No_LF = 0                    .49
     4       9519455 CLOB Line Count = 10000, Line_Last_Line_No_LF = 0                  9.08
     5      99609455 CLOB Line Count = 100000, Line_Last_Line_No_LF = 0                94.99


with sq as (select /*+ materialize */ segment_name, index_name from dba_lobs where owner='K' and table_name='TEST_TAB_LOB')
select t.segment_name, tablespace_name, header_file, relative_fno, header_block, bytes, blocks, extents, round(bytes/1024/1024) mb from dba_segments t, sq 
where t.segment_name = sq.segment_name or t.segment_name = sq.index_name;

  SEGMENT_NAME                      TABLESPACE_NAME      HEADER_FILE RELATIVE_FNO HEADER_BLOCK      BYTES     BLOCKS    EXTENTS         MB
  --------------------------------- -------------------- ----------- ------------ ------------ ---------- ---------- ---------- ----------
  SYS_IL0005911971C00002$$          U1                            22         1024      2830970   10485760       1280         25         10
  SYS_LOB0005911971C00002$$         U1                            22         1024      2830962 1143996416     139648        201       1091

(see Blog: "One Oracle CLOB Space Usage Test": 
"https://ksun-oracle.blogspot.com/2025/02/one-oracle-clob-space-usage-test.html")


2. SQL Recursive Query


2.1 SQL Slow Recursive


In Post: "Reading clob line by line with pl\sql"
(https://stackoverflow.com/questions/11647041/reading-clob-line-by-line-with-pl-sql),
there is one ANSI Standard Recursive Query to read clob line by line:

create or replace procedure SQL_SLOW (p_id number) as
  --v_tmp clob := 'aaaa'||chr(10)||'bbb'||chr(10)||'ccccc';
  v_tmp clob;
  l_cnt number := 0;
  l_len number := 0;
begin
  select mylob into v_tmp from test_tab_lob where id = p_id;
  for rec in (with clob_table(c) as (select v_tmp c from dual)
            select regexp_substr(c, '.+', 1, level) text,level line
             from clob_table
          connect by level <= regexp_count(c, '.+')) 
  loop
    --dbms_output.put_line(rec.text);
    l_cnt := l_cnt+1;
    l_len := l_len+length(rec.text);
  end loop;
  dbms_output.put_line('CNT = '||l_cnt ||', Length = '||l_len);
end;
/

-- exec SQL_SLOW(1);
This code has Quadratic Performance (O(n^2)). Each line is read from first CLOB line position, hence n*n/2 scans.
Session shows intensive Oracle Wait Event "direct path read".

With perf tool, we observe following top Oracle subroutine calls:

$ > perf top -p 2450579 -d 2 

    PerfTop:     744 irqs/sec  kernel: 0.0%  exact:  0.0% lost: 0/0 drop: 0/0 [4000Hz cpu-clock:uhpppH],  (target_pid: 2450579)
-------------------------------------------------------------------------------------------------------------------------------------

    10.72%  oracle         [.] lxregmatch
     3.88%  oracle         [.] sxorchk
     3.17%  oracle         [.] lxregmatpush
     3.04%  oracle         [.] lxoCntByte
     2.71%  oracle         [.] lxpoCmpStr


2.2 SQL Fast Recursive


create or replace procedure SQL_FAST (p_id number) as
  l_cnt number := 0;
  l_len number := 0;
begin
  for c in (
    with 
      --lob as (select 1 id, length(mylob)+1 lob_len, mylob||chr(10) mylob from 
      --    (select to_clob('abc'||chr(10)||'x1'||chr(10)||chr(10)||'x2'||chr(10)||'x3') mylob from dual))
      lob as (select id, length(mylob)+1 lob_len, mylob||chr(10) mylob from test_tab_lob where id = p_id)
     ,recur_tab(id, lob_len, mylob, line, total_len, lf_pos, lf_pos_prior, lvl) as (
        select id, lob_len, mylob, dbms_lob.substr(mylob, instr(mylob, chr(10), 1, 1)-1, 1) line
              ,0 total_len, instr(mylob, chr(10), 1, 1) lf_pos, 0 lf_pos_prior, 0 lvl 
          from lob
        union all
        select id, lob_len, mylob, dbms_lob.substr(mylob, lf_pos - 1 - lf_pos_prior, lf_pos_prior+1) line
              ,total_len + length(line) + 1 total_len
              ,instr(mylob, chr(10), lf_pos+1, 1) lf_pos, lf_pos lf_pos_prior, lvl+1 lvl
          from recur_tab where total_len <= length(mylob))
    select t.*, length(line) line_len, dbms_lob.substr(line, length(line), 1) line_text 
      from recur_tab t where line is not null and lf_pos_prior > 0)
  loop
    l_cnt := l_cnt+1;
    l_len := l_len+length(c.line);
  end loop;
  dbms_output.put_line('CNT = '||l_cnt ||', Length = '||l_len);
end;
/

-- exec SQL_FAST(1);
The above code has Linear Prformance (O(n)). Each line is read from next CLOB line position, hence n scans.

Post: "Oracle: Read from CLOB column line by line and insert to the table"
(https://dba.stackexchange.com/questions/10893/oracle-read-from-clob-column-line-by-line-and-insert-to-the-table)
also has some similar code, which wrote:
"This is not a full answer because it only works if your clob is less than 4000 chars."


3. Plsql Implementations


3.1 Plsql Fast Version


In Post: "Reading clob line by line with pl\sql"
(https://stackoverflow.com/questions/11647041/reading-clob-line-by-line-with-pl-sql),
there is one Plsql code: "procedure parse_clob" as follows:

create or replace procedure PLSQL_FAST (p_id number) is
  p_clob         clob;
  l_offset       pls_integer:=1;
  l_line         varchar2(32767);
  l_total_length pls_integer := length(p_clob);
  l_line_length  pls_integer;
  l_cnt          number := 0;
  l_len          number := 0;
begin
  select mylob into p_clob from test_tab_lob where id = p_id;
  l_total_length := length(p_clob);
  dbms_output.put_line('l_total_length = '|| l_total_length); 
  while l_offset <= l_total_length loop
    l_line_length := instr(p_clob, chr(10), l_offset) - l_offset;
    if l_line_length < 0 then
      l_line_length := l_total_length + 1 - l_offset;
    end if;
    l_line:=substr(p_clob, l_offset, l_line_length);
    --dbms_output.put_line(l_line); --do line processing
    l_offset:=l_offset + l_line_length + 1;
    
    l_cnt := l_cnt + 1;
    l_len := l_len + l_line_length;
  end loop;
  
  dbms_output.put_line('CNT = '||l_cnt ||', Length = '||l_len);
end;
/

-- exec PLSQL_FAST(1);
Each line is read from next CLOB line offset.


3.2. Plsql Slow Version


create or replace procedure PLSQL_SLOW(p_id number) is
  p_clob         clob;
  l_offset       pls_integer := 1;
  l_line         varchar2(32767);
  l_total_length pls_integer := length(p_clob);
  l_line_length  pls_integer;
  l_lf_pos       pls_integer;
  l_cnt          number := 0;
  l_len          number := 0;
begin
  select mylob into p_clob from test_tab_lob where id = p_id;
  l_total_length := length(p_clob);
  dbms_output.put_line('l_total_length = '|| l_total_length); 
  while l_offset <= l_total_length loop
    l_lf_pos      := instr(p_clob, chr(10));
    l_line_length := l_lf_pos - 1;
    --dbms_output.put_line('l_offset = '||l_offset); 
    --dbms_output.put_line('l_line_length = '||l_line_length); 
    
    -- In the case of CLOB last line without CHR(10), l_line_length < 0.
    -- instr: If substring not found, INSTR will return 0, hence l_line_length = - l_offset. 
    if l_line_length < 0 then
      l_line_length := l_total_length + 1 - l_offset;
    end if;
    l_line := substr(p_clob, 1, l_line_length);
    --dbms_output.put_line('New l_line_length = '||l_line_length); 
    --dbms_output.put_line(l_line); --do line processing
    l_offset := l_offset + l_line_length + 1;
    p_clob := substr(p_clob, l_lf_pos + 1);
    
    l_cnt := l_cnt + 1;
    l_len := l_len + l_line_length;
  end loop;
  
  dbms_output.put_line('CNT = '||l_cnt ||', Length = '||l_len);
end;
/

-- exec PLSQL_SLOW(1);
CLOB loop calling of substr triggers many dynamic LOB Creations/Destructions in session PL/SQL
(Oracle subroutine "__intel_avx_rep_memcpy": UNIX memcpy).

perf tool shows top Oracle calls:

$ > perf top -p 2450579 -d 2 

  PerfTop:    3878 irqs/sec  kernel: 0.0%  exact:  0.0% lost: 0/0 drop: 0/37 [4000Hz cpu-clock:uhpppH],  (target_pid: 2450579)
-------------------------------------------------------------------------------------------------------------------------------------

    26.05%  oracle         [.] __intel_avx_rep_memcpy
     6.36%  oracle         [.] kcbgcur
     4.76%  oracle         [.] kcbgtcr
     4.05%  oracle         [.] kcbchg1_main
     3.55%  oracle         [.] kcbrls
Post "Testing Oracle's Use of Optane Persistent Memory, Part 1 - Low Latency Commits"
(https://tanelpoder.com/posts/testing-oracles-use-of-optane-persistent-memory/)
wrote: "This is the ìmemcpyî (actually __intel_avx_rep_memcpy)"


4. Test Outcome


4.1. SQL Slow


SQL > exec SQL_SLOW(3);
  CNT = 1,000, Length = 509,455
  Elapsed: 00:04:45.59

SQL > exec SQL_SLOW(4);
  CNT = 10,000, Length = 9,509,455
  Elapsed: 09:49:48.05

SQL > exec SQL_SLOW(5);
-- Not finished after 10 hours


4.2. SQL Fast


SQL > exec SQL_FAST(3);
  CNT = 1,000, Length = 509,455
  Elapsed: 00:00:00.67

SQL > exec SQL_FAST(4);
  CNT = 10,000, Length = 9,509,455
  Elapsed: 00:00:02.78

SQL > exec SQL_FAST(5);
  CNT = 100,000, Length = 99,509,455
  Elapsed: 00:00:39.74


4.3. Plsql Fast


SQL > exec PLSQL_FAST(3);
  CNT = 1,000, Length = 509,455
  Elapsed: 00:00:01.20

SQL > exec PLSQL_FAST(4);
  CNT = 10,000, Length = 9,509,455
  Elapsed: 00:00:04.93

SQL > exec PLSQL_FAST(5);
  CNT = 100,000, Length = 99,509,455
  Elapsed: 00:00:45.29


4.4. Plsql Slow


SQL > exec PLSQL_SLOW(3);
  CNT = 1,000, Length = 509,455
  Elapsed: 00:00:01.36

SQL > exec PLSQL_SLOW(4);
  CNT = 10,000, Length = 9,509,455
  Elapsed: 00:03:22.48

SQL > exec PLSQL_SLOW(5);
  CNT = 100,000, Length = 99,509,455
  Elapsed: 06:16:49.58


4.5. Performance Summary


CLOB_Lines = 1,000
   Code          Elapsed
   -----------   -----------
   SQL_SLOW      00:04:45.59
   SQL_FAST      00:00:00.67
   PLSQL_SLOW    00:00:01.36
   PLSQL_FAST    00:00:01.20    

CLOB_Lines = 10,000
   Code          Elapsed
   -----------   -----------
   SQL_SLOW      09:49:48.05    *** 9 hours
   SQL_FAST      00:00:02.78
   PLSQL_SLOW    00:03:22.48
   PLSQL_FAST    00:00:04.93 

CLOB_Lines = 100,000
   Code          Elapsed
   -----------   -----------
   SQL_SLOW      > 10 hours   *** > 10 hours
   SQL_FAST      00:00:39.74
   PLSQL_SLOW    06:16:49.58  *** 6 hours
   PLSQL_FAST    00:00:45.29 

Thursday, July 10, 2025

"library cache: bucket mutex X" on V$ Fixed Views: Case Test

Oracle DB experienced heavy "library cache: bucket mutex X" when V$ Fixed Views were queried frequently,
which impeded Oracle normal functions like background processes Mnnn and MZnn.

Note: Tested on Oracle 19c.


1. Test Setup


In the following code, we query V$LIBCACHE_LOCKS (similar behavior for DBA_KGLLOCK), which is a union of X$KGLLK and X$KGLPN.

create or replace procedure test_bucket_mutex(p_job_id number, p_loop_count number) as
begin
  for i in 1..p_loop_count loop
    for c in (select * from V$LIBCACHE_LOCKS where rownum <= p_job_id) loop
      null;
    end loop;
  end loop;
end;
/

-- exec test_bucket_mutex(1, 1);

create or replace procedure test_bucket_mutex_jobs(p_job_count number, p_loop_count number) as
begin
  for i in 1..p_job_count loop
    dbms_scheduler.create_job (
      job_name        => 'TEST_JOB_'||i,
      job_type        => 'PLSQL_BLOCK',
      job_action      => 'begin test_bucket_mutex('||i||', '||p_loop_count||'); end;',    
      start_date      => systimestamp,
      --repeat_interval => 'systimestamp',
      auto_drop       => true,
      enabled         => true);
  end loop;
end;
/


2. Test Run


Run a test with 16 Jobs:

exec test_bucket_mutex_jobs(16, 1e4);
AWR shows:

Top 10 Foreground Events by Total Wait Time

EventWaitsTotal Wait Time (sec)Avg Wait% DB timeWait Class
library cache: bucket mutex X939,40217.1K18.22ms59.8Concurrency
DB CPU 7062.2 24.7 
library cache: mutex X5,13969.313.48ms.2Concurrency
control file sequential read20,8742.9137.27us.0System I/O
db file sequential read3,1951.8557.97us.0User I/O
Disk file operations I/O2,602.272.88us.0User I/O
cursor: pin S8.117.55ms.0Concurrency
log file sync20.16.52ms.0Commit
direct path write590217.10us.0User I/O
latch free1010.66ms.0Other

SQL ordered by Elapsed Time

Elapsed Time (s)Executions Elapsed Time per Exec (s) %Total%CPU%IO SQL IdSQL ModuleSQL Text
28,575.23159,7100.1899.8924.660.009sz3zkc69bpjh DBMS_SCHEDULER SELECT * FROM V$LIBCACHE_LOCKS...
1,790.6211,790.626.2624.820.000f1dxm6hd2c2u DBMS_SCHEDULER DECLARE job BINARY_INTEGER := ...
1,790.1811,790.186.2624.650.005vp17fw0hgrz1 DBMS_SCHEDULER DECLARE job BINARY_INTEGER := ...

Mutex Sleep Summary

Mutex TypeLocationSleepsWait Time (ms)
Library Cachekglic1 491,696,79215,671,698
Library Cachekglic4 145172,6341,423,319
Library CachekglGetHandleReference 1238,26261,133
Library CachekglReleaseHandleReference 1241,1768,110
Library Cachekglhdgn1 62971
Cursor PinkksLockDelete [KKSCHLPIN6]9138
Cursor Pinkksfbc [KKSCHLFSP2]22
Row Cache[14] kqrScan10

Top SQL with Top Row Sources

SQL IDPlan HashExecutions% ActivityRow Source% Row SourceTop Event% EventSQL Text
9sz3zkc69bpjh2131580607279498.87 FIXED TABLE - FULL49.95library cache: bucket mutex X30.36 SELECT * FROM V$LIBCACHE_LOCKS...
FIXED TABLE - FULL48.57library cache: bucket mutex X31.00

During the test, we can monitor "library cache: bucket mutex X" waits by:


select 'BLOCKING_SESSION'   sess, program, event, mod(s.p1, power(2, 17)) "buckt muext(child_latch)", s.p1, s.p2, s.p3, s.sql_id, q.sql_text, m.*, s.*, q.* 
from v$session s, v$mutex_sleep_history m, v$sqlarea q
 where s.sid = m.blocking_session and s.sql_id = q.sql_id and m.sleep_timestamp > sysdate-5/1440 and m.sleeps > 3
union all
select 'REQUESTING_SESSION' sess, program, event, mod(s.p1, power(2, 17)) "buckt muext(child_latch)", s.p1, s.p2, s.p3, s.sql_id, q.sql_text, m.*, s.*, q.* 
from v$session s, v$mutex_sleep_history m, v$sqlarea q
 where s.sid = m.requesting_session and s.sql_id = q.sql_id and m.sleep_timestamp > sysdate-5/1440 and m.sleeps > 3;

select bs.session_id, bs.session_serial#, bs.program, bs.event, bs.p1, bs.blocking_session, bs.blocking_session_serial#, bs.sql_id
      ,s.sample_time, s.session_id, s.session_serial#, s.program, s.event, s.p1, s.blocking_session, s.blocking_session_serial#, s.sql_id
  from v$active_session_history bs, v$active_session_history s
where s.event  = 'library cache: mutex X'
  and bs.event = 'library cache: bucket mutex X'
  and s.sample_time = bs.sample_time
  and mod(s.p1, power(2, 17)) = bs.p1
  and s.session_id != bs.session_id
  and s.sample_time > sysdate-5/1440
order by s.sample_time desc, bs.session_id, s.session_id;

select sql_id, last_active_time, executions, disk_reads, direct_writes, buffer_gets, rows_processed, sql_text, v.* 
from v$sqlarea v where sql_id in ('9sz3zkc69bpjh');


3. "library cache: bucket mutex X" Tracing


Open one Sqlplus window, execute bpftrace on its process (UNIX pid 293988):

bpftrace -e 'uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2, 
             uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2 / pid==293988 /
   {@ustack_cnt[probe] = count();}' 
Run query to fetch rows with rownum limit:

SQL > select * from V$LIBCACHE_LOCKS where rownum <= 10;
  10 rows selected.
Here bpftrace output:

	@ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2]: 123462
	@ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2]:       269774
Same bpftrace output for count query:

SQL > select count(*) from V$LIBCACHE_LOCKS where rownum <= 10;
    COUNT(*)
  ----------
          10
Run query without rownum limit, we get the similar output due to FIXED TABLE FULL on X$KGLLK and X$KGLPN.

SQL >  select * from V$LIBCACHE_LOCKS;
  1226 rows selected.
        
	@ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2]: 123488
	@ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2]:       269939
Same bpftrace output for count query:

SQL > select count(*) from V$LIBCACHE_LOCKS;
    COUNT(*)
  ----------
        1226
To get Parameter P1 and P3 of "library cache: bucket mutex X", we can use bpftrace script:

bpftrace -e 'uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2 / pid==293988 /
   {@ustack_cnt["kglGetBucketMutex", reg("si"), reg("r8")] = count();}'
Pick a few output lines (first number is P1, second is P3):

  @ustack_cnt[kglGetBucketMutex, 6574, 145]: 2
  @ustack_cnt[kglGetBucketMutex, 5442, 49]:  2
  @ustack_cnt[kglGetBucketMutex, 3311, 49]:  2
Then we can find them in v$db_object_cache:

select child_latch, hash_value, mod(hash_value, power(2, 17)) bucket_p1, owner, substr(name, 1, 50) name, namespace, type 
from v$db_object_cache t where child_latch in (
6574,
5442,
3311
);  

CHILD_LATCH HASH_VALUE  BUCKET_P1 OWNER NAME                                          NAMESPACE            TYPE
----------- ---------- ---------- ----- --------------------------------------------- -------------------- ----------
       3311 1185156335       3311 SYS   java/util/function/DoubleBinaryOperator       TABLE/PROCEDURE      JAVA CLASS
       5442 1797395778       5442       UPDATE SYS.WRI$_ADV_                          SQL AREA             CURSOR
       6574  363993518       6574       WITH binds as           (select :dbid         SQL AREA             CURSOR
To get Parameter P3 of library cache: bucket mutex X, we can use bpftrace script:

bpftrace -e 'uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2 / pid==293988 /
   {@ustack_cnt["kglGetBucketMutex", reg("r8")] = count();}'
   
  @ustack_cnt[kglGetBucketMutex, 64]:  1
  @ustack_cnt[kglGetBucketMutex, 62]:  9
  @ustack_cnt[kglGetBucketMutex, 49]:  57846
  @ustack_cnt[kglGetBucketMutex, 145]: 65558
We can see that bpftrace output [kglGetBucketMutex, 49] and [kglGetBucketMutex, 145] big numbers match "kglic1 49" and "kglic1 145" big stats in AWR - Mutex Sleep Summary.

If we query normal tables, there are much less kglGetBucketMutex and kglGetMutex:

SQL > select count(*) from dba_objects;

    COUNT(*)
  ----------
     2004127
   
bpftrace -e 'uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2, 
             uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2 / pid==293988 /
   {@ustack_cnt[probe] = count();}'
   
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2]: 1
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2]:       15


4. GV$LIBCACHE_LOCKS and DBA_KGLLOCK DDL and Xplan


Both V$LIBCACHE_LOCKS and SYS.DBA_KGLLOCK are union of X$KGLLK and X$KGLPN. Any queries on them are FIXED TABLE FULL on X$KGLLK and X$KGLPN (rownum limit has no effect).

------ V$LIBCACHE_LOCKS ------
SELECT INST_ID, 'LOCK', KGLLKADR, KGLLKUSE, KGLLKSES, KGLLKHDL, KGLLKPNC, KGLLKCNT, KGLLKMOD, KGLLKREQ, KGLLKSPN, CON_ID
  FROM X$KGLLK
UNION
SELECT INST_ID, 'PIN',  KGLPNADR, KGLPNUSE, KGLPNSES, KGLPNHDL, KGLPNLCK, KGLPNCNT, KGLPNMOD, KGLPNREQ, KGLPNSPN, CON_ID
  FROM X$KGLPN
  
select * from V$LIBCACHE_LOCKS where rownum <= :B1;

Plan hash value: 2131580607
 
---------------------------------------------------------------------------------------------------
| Id  | Operation             | Name              | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |                   | 20956 |  2414K|       |   868   (1)| 00:00:01 |
|*  1 |  COUNT STOPKEY        |                   |       |       |       |            |          |
|   2 |   VIEW                | GV$LIBCACHE_LOCKS | 20956 |  2414K|       |   868   (1)| 00:00:01 |
|*  3 |    SORT UNIQUE STOPKEY|                   | 20956 |  2361K|  3656K|   868   (1)| 00:00:01 |
|   4 |     UNION-ALL         |                   |       |       |       |            |          |
|*  5 |      FIXED TABLE FULL | X$KGLLK           | 18837 |  1048K|       |     0   (0)| 00:00:01 |
|*  6 |      FIXED TABLE FULL | X$KGLPN           |  2119 |   132K|       |     0   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------

------ DBA_KGLLOCK ------

CREATE OR REPLACE FORCE NONEDITIONABLE VIEW SYS.DBA_KGLLOCK
(KGLLKUSE, KGLLKHDL, KGLLKMOD, KGLLKREQ, KGLLKTYPE)
BEQUEATH DEFINER
AS 
  select kgllkuse, kgllkhdl, kgllkmod, kgllkreq, 'Lock' kgllktype from x$kgllk
    union all
  select kglpnuse, kglpnhdl, kglpnmod, kglpnreq, 'Pin'  kgllktype from x$kglpn;
  
  
select * from DBA_KGLLOCK where rownum <= :B1;

Plan hash value: 3293675002
 
-----------------------------------------------------------------------------------
| Id  | Operation           | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |             | 20956 |   982K|     0   (0)| 00:00:01 |
|*  1 |  COUNT STOPKEY      |             |       |       |            |          |
|   2 |   VIEW              | DBA_KGLLOCK | 20956 |   982K|     0   (0)| 00:00:01 |
|   3 |    UNION-ALL        |             |       |       |            |          |
|   4 |     FIXED TABLE FULL| X$KGLLK     | 18837 |   423K|     0   (0)| 00:00:01 |
|   5 |     FIXED TABLE FULL| X$KGLPN     |  2119 | 48737 |     0   (0)| 00:00:01 |
-----------------------------------------------------------------------------------


5. Related Work


(1). Dynamic_plan_table, x$kqlfxpl and extreme library cache latch contention (Posted by Riyaj Shamsudeen on March 13, 2009)
(https://orainternals.wordpress.com/tag/kglic/)

We traced following two queries with above bpftrace scripts,
and the output shows that kglGetBucketMutex requests are proptional to rownum limit (:B1).
(For V$LIBCACHE_LOCKS and DBA_KGLLOCK, kglGetBucketMutex requests are constant, irrelvant to rownum limit)

select count(*) from GV$SQL_PLAN where rownum <= :B1;       --FIXED TABLE FULL on X$KQLFXPL
select count(*) from GV$ALL_SQL_PLAN where rownum <= :B1;   --FIXED TABLE FULL on X$ALL_KQLFXPL
Here the test output:

bpftrace -e 'uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2,
             uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2 / pid==299591 /
   {@ustack_cnt[probe] = count();}'
Attaching 2 probes...

select count(*) from GV$SQL_PLAN where rownum <= 1;
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2]: 47
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2]:       111

select count(*) from GV$SQL_PLAN where rownum <= 10;
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2]: 117
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2]:       287

select count(*) from GV$SQL_PLAN where rownum <= 100;
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2]: 436
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2]:       974

select count(*) from GV$SQL_PLAN where rownum <= 1000;
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetBucketMutex+2]: 3523
  @ustack_cnt[uprobe:/orabin/app/oracle/product/19.27.0.0.250415-212/bin/oracle:kglGetMutex+2]:       7725
(2). Oracle PLITBLM "library cache: mutex X"
(https://ksun-oracle.blogspot.com/2021/04/oracle-plitblm-library-cache-mutex-x.html)

(3). Row Cache Object and Row Cache Mutex Case Study
(https://ksun-oracle.blogspot.com/2020/08/row-cache-object-and-row-cache-mutex.html)

(4). ORACLE MUTEXES, FRITS HOOGLAND (https://fritshoogland.wordpress.com/wp-content/uploads/2020/04/mutexes-2.pdf)


6. GDB script


We can also use following GDB script to trace "library cache: bucket mutex X":

---------------- bucket_mutex_1, gdb -x bucket_mutex_1 -p 293988 ----------------

set pagination off
set logging file bucket_mutex_1.log
set logging overwrite on
set logging on
set $kmutexget = 1
set $kbucketget = 1

break kglGetBucketMutex
command 
printf "------kglGetBucketMutex (%i) ---> Bucket (rsi): %d (%X), Location(r8d): %d (%X)\n", $kbucketget++, $rsi, $rsi, $r8d, $r8d
backtrace 4
continue
end

break kglGetMutex
command 
printf "------kglGetMutex (%i) ---> Mutex addr (rsi): %d (%X), Location(r8d): %d (%X)\n", $kmutexget++, $rsi, $rsi, $r8d, $r8d
continue
end


7. kglMutexLocations[] array


For all kglMutexLocations, we can try to list them with following command.
They often appear in AWR section "Mutex Sleep Summary" for Mutex Type: "Library Cache"
(or V$MUTEX_SLEEP / V$MUTEX_SLEEP_HISTORY.location).

define PrintkglMutexLocations
  set pagination off
  set $i = 0
  while $i < $arg0
    x /s *((uint64_t *)&kglMutexLocations + $i)
    set $i = $i + 1
  end
end

(gdb) PrintkglMutexLocations 150

0x15f42524:     "kglic1    49"
0x15f42a24:     "kglic2 127"
0x15f42b68:     "kglic3       144"
0x15f42b7c:     "kglic4       145"

(Only "kglic" Mutex are shown here)

Tuesday, July 1, 2025

Oracle dbms_hprof: Uncatchable Time in PL/SQL Nested Program Units

This Blog will demonstrate dbms_hprof uncatchable time in PL/SQL nested procedures and packages, which often caused misleading in elapsed time accounting, and in locating time consumption PL/SQL programs.

Note: Tested on Oracle 19c.


1. Test Code


In following code, we have one procedure which calls a nested procedure:

create or replace procedure hprof_plsql_proc_test(p_cnt_outer number, p_cnt_inner number)
is
  type t_rec is record(
             id      pls_integer
            ,name    varchar2(1000)
           );
  type t_rec_tab is table of t_rec index by pls_integer;

  l_rec       t_rec;
  l_rec_tab   t_rec_tab;
  l_row_cnt   number;

  procedure proc_nested(p_cnt_outer number, p_cnt_inner number)
  is
  begin
    for i in 1..p_cnt_outer loop    
      l_rec_tab := t_rec_tab();
      select /*+ Start */ count(*) into l_row_cnt from dual;
      for i in 1..p_cnt_inner loop
        l_rec := t_rec(
                id      => 1
               ,name    => lpad('A', 900, 'B')||i
              );
        l_rec_tab(i) := l_rec;
      end loop;
      select /*+ End */ count(*) into l_row_cnt from dual;
    end loop;
  end;

begin
  dbms_output.put_line('hprof_plsql_proc_test('||p_cnt_outer||', '||p_cnt_inner||')');

  proc_nested(p_cnt_outer, p_cnt_inner);
end;
/


2. Test Run with Unwrapped PL/SQL


Run above procedure with dbms_hprof, and create html hprofile:

create or replace directory TEST_DIR as '/testdb/oradata/hprof/';

set serveroutput on
declare
  l_test_dir        varchar2(100) := 'TEST_DIR';
  l_hprof_file_name varchar2(100) := 'hprof_plsql_proc_test_UnWrapped_1.hpf';
  l_runid           number;
begin
  dbms_hprof.start_profiling (
    location => l_test_dir,
    filename => l_hprof_file_name);

  hprof_plsql_proc_test(1e2, 1e5);

  dbms_hprof.stop_profiling;

  l_runid := dbms_hprof.analyze (
     location    => l_test_dir,
     filename    => l_hprof_file_name,
     run_comment => 'hprof_plsql_proc_test Test');

  dbms_output.put_line('l_runid=' || l_runid);
end;
/
HProf shows that HPROF_PLSQL_PROC_TEST elapsed time is sum of all its Children.

But for Subtreee PROC_NESTED, the sum of all its Children (6635+6253=12888) is much less than Subtreee time (12642594), 12629706 (12642594 - 12888) is unaccounted.

Parents and Children Elapsed Time (microsecs) Data


HPROF_PLSQL_PROC_TEST.HPROF_PLSQL_PROC_TEST (Line 1)


Subtree Ind% Function Ind% Descendants Ind% Calls Ind% Function Name SQL ID SQL TEXT
12652694 100% 10088 0.1% 12642606 100% 1 0.5% HPROF_PLSQL_PROC_TEST.HPROF_PLSQL_PROC_TEST (Line 1)
Parents:
12652694 100% 10088 100% 12642606 100% 1 100% ORACLE.root
Children:
12642594 100% 12629706 100% 12888 100% 1 100% HPROF_PLSQL_PROC_TEST.HPROF_PLSQL_PROC_TEST.PROC_NESTED (Line 13)
12 0.0% 2 100% 10 100% 1 100% SYS.DBMS_OUTPUT.PUT_LINE (Line 109)
HPROF_PLSQL_PROC_TEST.HPROF_PLSQL_PROC_TEST.PROC_NESTED (Line 13)
Subtree Ind% Function Ind% Descendants Ind% Calls Ind% Function Name SQL ID SQL TEXT
12642594 100% 12629706 99.8% 12888 0.1% 1 0.5% HPROF_PLSQL_PROC_TEST.HPROF_PLSQL_PROC_TEST.PROC_NESTED (Line 13)
Parents:
12642594 100% 12629706 100% 12888 100% 1 100% HPROF_PLSQL_PROC_TEST.HPROF_PLSQL_PROC_TEST (Line 1)
Children:
6635 51.5% 6635 100% 0 N/A 100 100% HPROF_PLSQL_PROC_TEST.__static_sql_exec_line26 (Line 26)3cfwwbgj9ft44SELECT /*+ End */ COUNT(*) FROM DUAL
6253 48.5% 6253 100% 0 N/A 100 100% HPROF_PLSQL_PROC_TEST.__static_sql_exec_line18 (Line 18)8tuspbw8fxyrnSELECT /*+ Start */ COUNT(*) FROM DUAL
Look HProf raw file, we can see that time "P#X 139321" is not attributed to any program (PL/SQL or SQL):

P#C PLSQL."K"."HPROF_PLSQL_PROC_TEST"::7."HPROF_PLSQL_PROC_TEST.PROC_NESTED"#3048d2af80817a01 #13
P#X 8
P#C SQL."K"."HPROF_PLSQL_PROC_TEST"::7."__static_sql_exec_line18" #18."8tuspbw8fxyrn"
P#! SELECT /*+ Start */ COUNT(*) FROM DUAL
P#X 46
P#R
P#X 139321
P#C SQL."K"."HPROF_PLSQL_PROC_TEST"::7."__static_sql_exec_line26" #26."3cfwwbgj9ft44"
P#! SELECT /*+ End */ COUNT(*) FROM DUAL
P#X 73

3. Test Run with Wrapped PL/SQL

Install wrapped PL/SQL (see Section 4. Wrapped PL/SQL), and run it with dbms_hprof, then create html hprofile:

set serveroutput on
declare
  l_test_dir        varchar2(100) := 'AAA_RAC_XCHNG';
  l_hprof_file_name varchar2(100) := 'hprof_plsql_proc_test_Wrapped_1.hpf';
  l_runid           number;
begin
  dbms_hprof.start_profiling (
    location => l_test_dir,
    filename => l_hprof_file_name);

  hprof_plsql_proc_test(1e2, 1e5);

  dbms_hprof.stop_profiling;

  l_runid := dbms_hprof.analyze (
     location    => l_test_dir,
     filename    => l_hprof_file_name,
     run_comment => 'hprof_plsql_proc_test Test');

  dbms_output.put_line('l_runid=' || l_runid);
end;
/
HProf shows that the top call: HPROF_PLSQL_PROC_TEST has time = 12865442, but all its Children has only 13408.
The unaccounted time amounts to 12852034 (12865442-13408), almost no time is profiled.

Parents and Children Elapsed Time (microsecs) Data


HPROF_PLSQL_PROC_TEST.HPROF_PLSQL_PROC_TEST (Line 1)


Subtree Ind% Function Ind% Descendants Ind% Calls Ind% Function Name SQL ID SQL TEXT
12865442 100% 12852034 99.9% 13408 0.1% 1 0.5% HPROF_PLSQL_PROC_TEST.HPROF_PLSQL_PROC_TEST (Line 1)
Parents:
12865442 100% 12852034 100% 13408 100% 1 100% ORACLE.root
Children:
6879 51.3% 6879 100% 0 N/A 100 100% HPROF_PLSQL_PROC_TEST.__static_sql_exec_line26 (Line 26)3cfwwbgj9ft44SELECT /*+ End */ COUNT(*) FROM DUAL
6517 48.6% 6517 100% 0 N/A 100 100% HPROF_PLSQL_PROC_TEST.__static_sql_exec_line18 (Line 18)8tuspbw8fxyrnSELECT /*+ Start */ COUNT(*) FROM DUAL
12 0.1% 1 100% 11 100% 1 100% SYS.DBMS_OUTPUT.PUT_LINE (Line 109)

4. Wrapped PL/SQL


testdb $ wrap iname=hprof_plsql_proc_test.sql

testdb $ cat hprof_plsql_proc_test.plb
create or replace procedure k.hprof_plsql_proc_test wrapped
a000000
1f
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
7
3ab 233
YPWOC06TiznHzO1ynQEtqQmVKQ4wg41eLq5qfHRDNA/uugXMeaR4dUB33xnjiWxc7OHa6eMo
UnVwftozxoagoNnUy3AmGAHaM04S3mtcStgl57sTzcETVdi/D/YMuQNVnbkzODjH3tjldCfI
PWWEpIT9hgkzLRHd2fupk5Tn1CdHN5xnByVWGlMXbg0XqPL8cLg3kk3KpynTkMdP/OTRPeru
FKHxhWT06ICM2KGCeErLNw6LpKb6pfPCjSTeew4TUaHEPIncmHDrvctMqtP8r80M9J+x6KGz
64t61S0hhflZb7OPfZe4rwEDMXHejzaIG4z3mqIWXFPkcpJbiXgDHUdLqNuT9OHHHYbubIn4
sM0cQAuFXGwuV19NmMwZe8c7p93lw/WcqgsRNdp0AHNiQlXStnkAQhfEfK0uMyUJCY4+mdYh
4+57oeDnwsrl9j/AjJrZBXpd9/2We+5ua5Gyl4Ihj8bZJFql/feu8r/TLX6Tey4Yl199A6wM
dNZOnVlpsUFAMi4MM2tACZ7pNXedjIJkwX0xaWMdxwiydIiGxw==

/

5. PL/SQL Package Test Cdoe

The similar behaviour can be demonstrated for nested procedure/function in PL/SQL package.

create or replace package hprof_plsql_pkg_test
is
  procedure proc_outer(p_cnt_outer number, p_cnt_inner number);
end;
/

create or replace package body hprof_plsql_pkg_test
is
  type t_rec is record(
             id      pls_integer
            ,name    varchar2(1000)
           );
  type t_rec_tab is table of t_rec index by pls_integer;

  l_rec       t_rec;
  l_rec_tab   t_rec_tab;
  l_row_cnt   number;

  procedure proc_nested(p_cnt_outer number, p_cnt_inner number)
  is
  begin
    for i in 1..p_cnt_outer loop    
      l_rec_tab := t_rec_tab();
      select /*+ Start */ count(*) into l_row_cnt from dual;
      for i in 1..p_cnt_inner loop
        l_rec := t_rec(
                id      => 1
               ,name    => lpad('A', 900, 'B')||i
              );
        l_rec_tab(i) := l_rec;
      end loop;
      select /*+ End */ count(*) into l_row_cnt from dual;
    end loop;
  end;
  
  procedure proc_outer(p_cnt_outer number, p_cnt_inner number)
  is
  begin
    proc_nested(p_cnt_outer, p_cnt_inner);
  end;
end;
/

Tuesday, June 17, 2025

ORA-01002: fetch out of sequence: One Case Test


drop table test_tab;
 
create table test_tab (x number, y varchar2(100)); 

declare
  l_cnt number := 0;
begin
  execute immediate 'truncate table test_tab';
  insert into test_tab select level x, lpad('A', 90, 'B')||level from dual connect by level <= 500;
  
  for c in (select x, y from test_tab) loop
    l_cnt := l_cnt + 1;
    dbms_output.put_line(c.x);
    if c.x > 300 then
      rollback;
    end if;
  end loop;
  
  exception when others then
    dbms_output.put_line('Row CNT = '||l_cnt);
    raise;
end;
/

--------------- Test Output -----------------
310
311
312
313
Row CNT = 100
declare
*
ERROR at line 1:
ORA-01002: fetch out of sequence
ORA-06512: at line 17
ORA-06512: at line 7
ORA-06512: at line 7
For Temporary Table ORA-01002, see Blog: One Test on the Different Errors of Oracle Global Temporary Tables vs. Private Temporary Tables
(https://ksun-oracle.blogspot.com/2024/01/one-test-on-different-errors-of-oracle.html)