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)

Friday, May 23, 2025

Oracle ORA-00600 [pfrsfm: stack mismatch] and [pfrsfm: stack mismatch] Reproducing

This Blog will demonstrate how to generate Oracle ORA-00600 [pfrsfm: stack mismatch] and [pfrsfm: stack mismatch].

Note: Tested on Oracle 19c


1. ORA-00600: internal error code, arguments: [pfrsfm: stack mismatch]


Open one Sqlplus window, start 400 Jobs to test (see Section 3. Test Code):

  SQL > exec start_test(400, 1e4, 1e6, 0, 5);
During and after test (it takes about 15 minutes), check DB alert.log and incident files if they contain error text like:

  ORA-00600: internal error code, arguments: [pfrsfm: stack mismatch]
If with 400 Jobs, it is not reproduced, increase it and re-run the test.

Here the incident file:

ORA-00600: internal error code, arguments: [pfrsfm: stack mismatch], [0x7F1F08BB3BF8], [0xFFFFFFFFFFFFFFFF], [0x7F1F08506000], [0x7F1F08BB33F0], 
                                                                     [0xFFFFFFFFFFFFFFFF], [0xFFFFFFFFFFFFFFFF], [], [], [], [], []
ORA-27403: scheduler stop job event
ORA-27403: scheduler stop job event

========= Dump for incident 14217 (ORA 600 [pfrsfm: stack mismatch]) ========

----- Current SQL Statement for this session (sql_id=f44xkpcgqsatb) -----
SELECT * FROM TABLE(PIPELINE_TAB(:B1 , :B2 )) WHERE ROWNUM <= :B1 -5

----- Parser State -----
Parser state1: len1=0 len2=0 pos1=0 pos2=0
Parser state2: flg=0x0 xflg=0x0 xxflg=0x400
Parser state3: tty=0 tlen=0
Parser string: prx=0x7f1f0e8a9d60 base=(nil) cur=(nil)

----- PL/SQL Call Stack -----
  object      line  object
  handle    number  name
0x9def1680         5  procedure K.TEST_JOB_PROC
0x7e82e800         1  anonymous block

[TOC00006]
----- Call Stack Trace -----
calling              call     entry                argument values in hex      
location             type     point                (? means dubious value)     
-------------------- -------- -------------------- ----------------------------
dbgexProcessError()  call     dbgexPhaseII()       7F1F0E83B6D8 7F1F0E7ED1F0
+1872                                              7FFD1ED4C800 7FFD1ED450C8 ?
                                                   000000000 ? 000000000 ?
dbgePostErrorKGE()+  call     dbgexProcessError()  7F1F0E83B6D8 7F1F0E7ED1F0
1853                                               000000001 000000000
                                                   000000000 ? 000000000 ?
dbkePostKGE_kgsf()+  call     dbgePostErrorKGE()   7F1F0E87B9C0 7F1F08BB0050
71                                                 000000258 000000000 ?
                                                   000000000 ? 000000000 ?
kgeadse()+448        call     dbkePostKGE_kgsf()   7F1F0E87B9C0 7F1F08BB0050
                                                   000000258 000000000 ?
                                                   000000000 ? 000000000 ?
kgerinv_internal()+  call     kgeadse()            7F1F0E87B9C0 ? 7F1F08BB0050
44                                                 000000258 ? 016036C24
                                                   000000000 000000006
kgerinv()+40         call     kgerinv_internal()   7F1F0E87B9C0 ? 7F1F08BB0050 ?
                                                   000000258 ? 016036C24 ?
                                                   000000000 ? 000000006 ?
kgeasnmierr()+146    call     kgerinv()            7F1F0E87B9C0 ? 7F1F08BB0050 ?
                                                   000000258 ? 016036C24 ?
                                                   000000000 ? 000000006 ?
pfrsfm()+1233        call     kgeasnmierr()        7F1F0E87B9C0 ? 7F1F08BB0050 ?
                                                   000000258 ? 016036C24 ?
                                                   000000002 7F1F08BB3BF8
pfrspopstks()+103    call     pfrsfm()             7F1F0E87B9C0 ? 7F1F08BB0050 ?
                                                   000000258 ? 016036C24 ?
                                                   000000002 ? 7F1F08BB3BF8 ?
kksumc()+526         call     pfrspopstks()        7F1F0E87B9C0 ? 7F1F08507910
                                                   000000258 ? 016036C24 ?
                                                   000000002 ? 7F1F08BB3BF8 ?
opiodr()+4705        call     kksumc()             7F1F0864F5C8 000000005


2. ORA-00600: internal error code, arguments: [pfrsfm: Stack disordered]


Open one Sqlplus window, start test by:

SQL>  exec test_job_proc(1, 1e4, 1e6, 0, 5);
Get its session SPID (1055599) and its (sid, serial#) = (915.49648).

In UNIX Terminal, run kill command:

  kill -URG 1055599
or In another Sqlplus window, run cancel statement:

  alter system cancel sql '915,49648';
(see Blog:
   How to CANCEL a query running in another session? (https://tanelpoder.com/2010/02/17/how-to-cancel-a-query-running-in-another-session/)
   DBMS_SCHEDULER Job Not Running and Used Slaves (https://ksun-oracle.blogspot.com/2021/01/dbmsscheduler-job-not-running-and-used.html)
)

You will see either:

SQL>  exec test_job_proc(1, 1e4, 1e6, 0, 5);
BEGIN test_job_proc(1, 1e4, 1e6, 0, 5); END;

*
ERROR at line 1:
ORA-01013: user requested cancel of current operation
or

SQL>  exec test_job_proc(1, 1e4, 1e6, 0, 5);
BEGIN test_job_proc(1, 1e4, 1e6, 0, 5); END;

*
ERROR at line 1:
ORA-00603: ORACLE server session terminated by fatal error
ORA-24557: error 600 encountered while handling error 1013; exiting server
process
ORA-00600: internal error code, arguments: [pfrsfm: Stack disordered],
[0x7F0FE0BA14F8], [0x7F0FE0BA22F0], [0x7F0FE0BA1850], [0x7F0FE0BA34F8], [], [],
[], [], [], [], []
ORA-01013: user requested cancel of current operation
Process ID: 1055599
Session ID: 915 Serial number: 49648
If it is the first case, in the same Oracle session (ORA-01013 does not terminate session), re-run:

   exec test_job_proc(1, 1e4, 1e6, 0, 5); 
re-run UNIX kill command or SQL cancel statement.

Repeat above steps till the session hit "ORA-00600: [pfrsfm: Stack disordered]".

Then the session incident file shows:

ORA-00600: internal error code, arguments: [pfrsfm: Stack disordered], [0x7F0FE0BA14F8], [0x7F0FE0BA22F0], [0x7F0FE0BA1850], [0x7F0FE0BA34F8], 
                                                                       [], [], [], [], [], [], []
ORA-01013: user requested cancel of current operation

========= Dump for incident 11485 (ORA 600 [pfrsfm: Stack disordered]) ========

*** 2025-05-20T09:54:22.541566+02:00
dbkedDefDump(): Starting incident default dumps (flags=0x2, level=3, mask=0x0)
[TOC00003]
----- Current SQL Statement for this session (sql_id=b7vzj4s3qw97g) -----
SELECT * FROM TABLE(GET_TAB_PTF(:B1 , :B2 )) WHERE ROWNUM <= :B1 -5
[TOC00004]
----- Parser State -----
Parser state1: len1=0 len2=36 pos1=0 pos2=5
Parser state2: flg=0x0 xflg=0x4300000 xxflg=0x400
Parser state3: tty=0 tlen=5
Parser string: prx=0x7f0fe6887d60 base=(nil) cur=0x7ffce65bdbed

----- PL/SQL Call Stack -----
  object      line  object
  handle    number  name
0xa0fb7940         5  procedure K.TEST_ORA_600_DISCONNET
0x95391e58         1  anonymous block

----- Call Stack Trace -----
calling              call     entry                argument values in hex      
location             type     point                (? means dubious value)     
-------------------- -------- -------------------- ----------------------------
dbgexProcessError()  call     dbgexPhaseII()       7F0FE68196D8 7F0FE67CB1F0
+1872                                              7FFCE65B7000 7FFCE65AF908 ?
                                                   000000000 ? 000000000 ?
dbgePostErrorKGE()+  call     dbgexProcessError()  7F0FE68196D8 7F0FE67CB1F0
1853                                               000000001 000000000
                                                   000000000 ? 000000000 ?
dbkePostKGE_kgsf()+  call     dbgePostErrorKGE()   7F0FE68599C0 7F0FE0B90050
71                                                 000000258 000000000 ?
                                                   000000000 ? 000000000 ?
kgeadse()+448        call     dbkePostKGE_kgsf()   7F0FE68599C0 7F0FE0B90050
                                                   000000258 000000000 ?
                                                   000000000 ? 000000000 ?
kgerinv_internal()+  call     kgeadse()            7F0FE68599C0 ? 7F0FE0B90050
44                                                 000000258 ? 016036BEC
                                                   000000000 000000004
kgerinv()+40         call     kgerinv_internal()   7F0FE68599C0 ? 7F0FE0B90050 ?
                                                   000000258 ? 016036BEC ?
                                                   000000000 ? 000000004 ?
kgeasnmierr()+146    call     kgerinv()            7F0FE68599C0 ? 7F0FE0B90050 ?
                                                   000000258 ? 016036BEC ?
                                                   000000000 ? 000000004 ?
pfrsfm()+1055        call     kgeasnmierr()        7F0FE68599C0 ? 7F0FE0B90050 ?
                                                   000000258 ? 016036BEC ?
                                                   000000002 7F0FE0BA14F8
pfrspopstks()+103    call     pfrsfm()             7F0FE68599C0 ? 7F0FE0B90050 ?
                                                   000000258 ? 016036BEC ?
                                                   000000002 ? 7F0FE0BA14F8 ?
kksumc()+526         call     pfrspopstks()        7F0FE68599C0 ? 7F0FE0BA22F0
                                                   000000258 ? 016036BEC ?
                                                   000000002 ? 7F0FE0BA14F8 ?
opiodr()+4705        call     kksumc()             7F0FE0298488 000000005
                                                   000000258 ? 7F0FE68599C0
                                                   000000002 ? 7F0FE0BA14F8 ?
If we compare incident file of [pfrsfm: stack mismatch] and [pfrsfm: Stack disordered], we can see that they hit ORA-00600 at different pfrsfm locations:

pfrsfm()+1233

pfrsfm()+1055


3. Test Code



create or replace procedure clearup_test as
begin
  for c in (select * from dba_scheduler_jobs where job_name like '%TEST_JOB%') loop
    begin
      --set DBA_SCHEDULER_JOBS.enabled=FALSE
	    dbms_scheduler.disable (c.job_name, force => true, commit_semantics =>'ABSORB_ERRORS');
	    --set DBA_SCHEDULER_JOBS.enabled=TRUE, so that it can be scheduled to run (state='RUNNING')
	    --  dbms_scheduler.enable (c.job_name, commit_semantics =>'ABSORB_ERRORS');
	  exception when others then null;
	  end;
	end loop;
	
  for c in (select * from dba_scheduler_running_jobs where job_name like '%TEST_JOB%') loop
    begin
      --If force=FALSE, gracefully stop the job, slave process can update the status of the job in the job queue.
      --If force= TRUE, the Scheduler immediately terminates the job slave.
      --For repeating job with attribute "start_date => systimestamp" and enabled=TRUE, 
      --re-start immediate (state changed from 'SCHEDULED' to 'RUNNING'), DBA_SCHEDULER_JOBS.run_count increases 1.
	    dbms_scheduler.stop_job (c.job_name, force => true, commit_semantics =>'ABSORB_ERRORS');
	  exception when others then null;
	  end;
	end loop;
	
  for c in (select * from dba_scheduler_jobs where job_name like '%TEST_JOB%') loop
    begin
      --If force=TRUE, the Scheduler first attempts to stop the running job instances 
      --(by issuing the STOP_JOB call with the force flag set to false), and then drops the jobs.
	    dbms_scheduler.drop_job (c.job_name, force => true, commit_semantics =>'ABSORB_ERRORS');
	  exception when others then null;
	  end;
	end loop;
end;
/

drop type t_tab;
drop type t_row;

create type t_row as object (id number, name varchar2(50));
/

create type t_tab is table of t_row;
/

create or replace function pipeline_tab (p_rows in number, p_seconds number) return t_tab pipelined as
begin
  for i in 1 .. p_rows loop
    dbms_output.put_line('row: ' || i);
    pipe row(t_row(i, 'Name_' || i));
    if p_seconds > 0 then 
      dbms_session.sleep(p_seconds);
    end if;
  end loop;
  return;
end;
/

-- not needed
drop table test_tab;
create table test_tab(id number, label varchar2(10));
insert into test_tab(id, label) values(1, 'label');
commit;

create or replace procedure test_pipeline_proc_recur(p_rows number, p_seconds number, p_depth number) as
begin
  if p_depth = 0 then
    for c in (select * from table(pipeline_tab(p_rows, p_seconds)) where  rownum <= p_rows-5) 
    loop
      dbms_output.put_line('id = '||c.id);
    end loop;
  else 
    test_pipeline_proc_recur(p_rows, p_seconds, p_depth - 1);
  end if;
  exception when others then 
    dbms_output.put_line('p_depth = '||p_depth||', ERROR: ' || sqlerrm);
end;
/

create or replace procedure test_job_proc(p_job_id number, p_loop_count number, p_rows number, p_seconds number, p_depth number) as
begin
  update test_tab set id = id+1;
  for i in 1..p_loop_count loop
    execute immediate 'begin test_pipeline_proc_recur('||p_rows||', '||p_seconds||', '||p_depth||'); end; ';
  end loop;
  commit;
end;
/

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

create or replace procedure start_test(p_job_count number, p_loop_count number, p_rows number, p_seconds number, p_depth number) as
  l_cnt number := 0;
begin
  dbms_output.put_line('Start '||p_job_count||' Jobs at '||sysdate);
  start_jobs(p_job_count, p_loop_count, p_rows, p_seconds, p_depth);
  
  -- wait for all jobs running
  dbms_output.put_line('Wait all Jobs running at '||sysdate);
  loop
    dbms_session.sleep(1);
    select count(*) into l_cnt from dba_scheduler_running_jobs where job_name like 'TEST_JOB_CRASHED%';
    exit when l_cnt >= p_job_count;
  end loop;
  
  dbms_output.put_line('Start clearup at '||sysdate);
  clearup_test;
 
  -- wait for all jobs stopped
  dbms_output.put_line('Wait clearup at '||sysdate);
  loop
    dbms_session.sleep(1);
    select count(*) into l_cnt from dba_scheduler_running_jobs where job_name like 'TEST_JOB_CRASHED%';
    exit when l_cnt = 0;
  end loop;
  dbms_output.put_line('Test End at '||sysdate);
end;
/

-- exec start_jobs(1e2, 1e4, 1e6, 0, 5);
-- exec clearup_test;
-- exec test_job_proc(1, 1e4, 1e6, 0, 5);

--================== Test Run ==================--
--  exec start_test(400, 1e4, 1e6, 0, 5);

Tuesday, February 18, 2025

One Oracle CLOB Space Usage Test

In this Blog, we will make LOB space usage test to show the used_blocks, expired_blocks, unexpired_blocks.
And in case of LOB "buffer busy waits" and "latch: cache buffers chains", we list the type of LOB data blocks in buffer cache.

Note: Tested in Oraclw 19.25 with following UNDO parameters:
  temp_undo_enabled   boolean     TRUE
  undo_management     string      AUTO
  undo_retention      integer     3600


1. Test Run


Run following test to insert 1000 rows (TEST_SEQ is cycle with maxvalue 1000), each row has one CLOB of 1MB, so total is about 1000MB.

-- see appended Test Code
truncate table test_tab_lob;
set serveroutput on size unlimited 
alter sequence test_seq_1000 restart start with 1;
exec check_space_securefile_2('K', 'TEST_TAB_LOB', 'MYLOB');
-- 10000 inserts for total 1000 rows, each row is repeatedly overwritten.
exec test_lob_proc(1, 1000, 1e4); 
exec check_space_securefile_2('K', 'TEST_TAB_LOB', 'MYLOB');
select round(sum(dbms_lob.getlength(mylob))/1024/1024, 2) LOB_MB, round(sum(length(mylob))/1024/1024, 2) MB
      ,count(id), min(id), max(id), min(ts), max(ts) from test_tab_lob;
The output looks like:

SQL > exec test_lob_proc(1, 1000, 1e4);
  Each mylob Length = 1056000, SUM Size (MB) =10000
  SUM Size (MB) =10000, Real Contained Size (MB, due to TEST_SEQ_1000.Max Limit) = 1000    
  
  Elapsed: 00:08:02.09

SQL > exec check_space_securefile_2('K', 'TEST_TAB_LOB', 'MYLOB');
  ===========================================================================
  segment_size_blocks = 1301880
  used_blocks         = 145718 (11.19 %)
  expired_blocks      = 1156162 (88.81 %)
  unexpired_blocks    = 0 (0 %)
  ===========================================================================
  Segment Blocks/Bytes   = 1301880 / 10665000960 (10170.94 MB)
  Unused Blocks/Bytes    = 0 / 0 (0 %)
  Used Blocks/Bytes      = 145718 / 1193721856 (11.19 %)
  Expired Blocks/Bytes   = 1156162 / 9471279104 (88.81 %)
  Unexpired Blocks/Bytes = 0 / 0 (0 %)
  ===========================================================================
  NON Data Blocks  = 1156162 (88.81 %)
  NON_data_blocks_2 (= segment_size_blocks - used_blocks) = 1156162
  ===========================================================================
  LOBSEGMENT DBA_EXTENTS storage size Blocks  = 1301880, CNT = 280, MIN_Blocks = 120, MAX_Blocks = 8192
  LOBSEGMENT DBA_EXTENTS storage size Byets   = 10665000960 (10170.94 MB)
  LOBSEGMENT DBA_SEGMENTS storage size Blocks = 1301880
  LOBSEGMENT DBA_SEGMENTS storage size Byets  = 10665000960 (10170.94 MB)

SQL > select round(sum(dbms_lob.getlength(mylob))/1024/1024, 2) LOB_MB, round(sum(length(mylob))/1024/1024, 2) MB
             ,count(id), min(id), max(id), min(ts), max(ts) from test_tab_lob;

   LOB_MB         MB  COUNT(ID)  MIN(ID)  MAX(ID) MIN(TS)               MAX(TS)
  ------- ---------- ---------- -------- -------- --------------------  --------------------
  1007.08    1007.08       1000        1     1000 17-FEB-2025 11:14:27  17-FEB-2025 11:15:15
Above output shows that for 1007.08 MB CLOB data, the segment consumes about 10170.94 MB, of which 88.81% is used for NON Data Blocks, and they are expired_blocks.

By the way, previous Blog: "LOB ORA-22924: snapshot too old and Fix" (http://ksun-oracle.blogspot.com/2019/04/lob-ora-22924-snapshot-too-old-and-fix.html) showed the special behaviour of LOB snapshot too old.

We can also run a parallel job sessions to show 'buffer busy waits' / 'latch: cache buffers chains' and the contention data block CLASS#.

Run job test by:

--exec clearup_test;
truncate table test_tab_lob;
alter sequence test_seq_1000 restart start with 1;
exec check_space_securefile_2('K', 'TEST_TAB_LOB', 'MYLOB');
exec test_lob_proc_job(16, 1000, 1e3);

-- Wait till all Jobs finished
select count(*) from dba_scheduler_jobs where job_name like '%TEST_JOB%';

-- Show space usage
exec check_space_securefile_2('K', 'TEST_TAB_LOB', 'MYLOB');
And run monitoring query during test to watch the wait event:

select program, event, module, action, event, p1, p2, p3, p1text, p2text, p3text, t.* 
from v$active_session_history t
where sample_time > sysdate -10/1440 and action like 'TEST_JOB%' 
  and event in ('buffer busy waits', 'latch: cache buffers chains')
order by t.sample_time desc; 
It shows that most of 'buffer busy waits' / 'latch: cache buffers chains' are on LOB metadata CLASS# 8 (1st level bmb) and 6 (free list).

With following query, we can display the type of data blocks in buffer cache:

with sq as  (select /*+ materialize */ segment_name, index_name from dba_lobs where owner='K' and table_name='TEST_TAB_LOB'),
     obj as (select /*+ materialize */ object_name||', '||object_type LOB_INFO, object_id, data_object_id 
             from dba_objects t, sq where object_name = 'TEST_TAB_LOB' or object_name = sq.segment_name or object_name = sq.index_name)
select LOB_INFO, objd,  class#, status, dirty, file#, count(block#), count(distinct block#)  from v$bh, obj
where objd =  obj.data_object_id
group by LOB_INFO, objd,  class#, status, dirty, file# order by 1, 6 desc, 2, 3, 4, 5 desc; 
  
  LOB_INFO                                OBJD     CLASS# STATUS     D      FILE# COUNT(BLOCK#) COUNT(DISTINCTBLOCK#)
  --------------------------------- ---------- ---------- ---------- - ---------- ------------- ---------------------
  SYS_IL0005743559C00005$$, INDEX      5743561          1 xcur       N       1625             1                     1
  SYS_IL0005743559C00005$$, INDEX      5743561          4 xcur       N       1625             1                     1
  SYS_IL0005743559C00005$$, INDEX      5743561          8 xcur       N       1625             1                     1
  SYS_LOB0005743559C00005$$, LOB       5743564          4 cr         N       1625             5                     1
  SYS_LOB0005743559C00005$$, LOB       5743564          4 xcur       Y       1625             1                     1
  SYS_LOB0005743559C00005$$, LOB       5743564          6 cr         N       1625           205                    46
  SYS_LOB0005743559C00005$$, LOB       5743564          6 xcur       Y       1625            56                    56
  SYS_LOB0005743559C00005$$, LOB       5743564          8 cr         N       1625            34                     9
  SYS_LOB0005743559C00005$$, LOB       5743564          8 xcur       Y       1625             9                     9
  SYS_LOB0005743559C00005$$, LOB       5743564          9 cr         N       1625           424                   106
  SYS_LOB0005743559C00005$$, LOB       5743564          9 xcur       Y       1625           126                   126
  SYS_LOB0005743559C00005$$, LOB       5743564          9 xcur       N       1625            59                    59
  SYS_LOB0005743559C00005$$, LOB       5743564         10 cr         N       1625           115                    26
  SYS_LOB0005743559C00005$$, LOB       5743564         10 xcur       Y       1625            50                    50
  SYS_LOB0005743559C00005$$, LOB       5743564         12 xcur       N       1625          1271                  1271
  TEST_TAB_LOB, TABLE                  5743565          1 cr         N       1625           114                    28
  TEST_TAB_LOB, TABLE                  5743565          1 xcur       Y       1625            28                    28
  TEST_TAB_LOB, TABLE                  5743565          4 cr         N       1625             1                     1
  TEST_TAB_LOB, TABLE                  5743565          4 xcur       Y       1625             1                     1
  TEST_TAB_LOB, TABLE                  5743565          8 xcur       Y       1625             2                     2
  TEST_TAB_LOB, TABLE                  5743565          9 xcur       Y       1625             1                     1
  
  21 rows selected.
The above output shows that majority blocks are from CLASS# 12 (bitmap index block) and CLASS# 9 (2nd level bmb), and hardly see blocks of CLASS# 1 (data block), because Oracle uses "direct path" for LOB data (not IN ROW short LOB).

We can also dump the segment header and bitmap blocks to reveal space distribution.

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 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
  -------------------------- ---------------- ----------- ------------ ------------ ---------- ---------- ----------
  SYS_LOB0005743559C00005$$  TEST_TBS                1625         1024       262153 1.0661E+10    1301376        276
  SYS_IL0005743559C00005$$   TEST_TBS                1625         1024       262274      65536          8          1


alter session set max_dump_file_size = UNLIMITED;
alter session set tracefile_identifier = 'TEST_TAB_LOB_SEGMENT_DUMP_3';
EXECUTE DBMS_SPACE_ADMIN.SEGMENT_DUMP('TEST_TBS', 1024, 262153); 

-- Dump DDL
select dbms_metadata.get_ddl('TABLE', 'TEST_TAB_LOB', 'KS') from dual;
or dump single bmb block:

select * from  v$bh where objd=5743591 and class#=8 and status='xcur';
  -- 1625	262159	8	xcur
alter session set max_dump_file_size = UNLIMITED;
alter session set tracefile_identifier = 'bmb_1_262159_dump';
alter system dump datafile 1625 block 262159; 
In above test, the CLOB retention_type is DEFAULT:

select t.column_name, t.retention_type, retention_value, s.segment_name, s.tablespace_name, s.header_file, s.relative_fno, s.header_block--, t.*
from dba_lobs t, dba_segments s where t.owner='K' and t.table_name='TEST_TAB_LOB' and t.segment_name = s.segment_name;

  COLUMN_NAME  RETENTION_TYPE  RETENTION_VALUE SEGMENT_NAME               TABLESPACE_NAME   HEADER_FILE RELATIVE_FNO HEADER_BLOCK
  ------------ --------------- --------------- -------------------------- ----------------- ----------- ------------ ------------
  MYLOB        DEFAULT                         SYS_LOB0005743559C00005$$  TEST_TBS                 1625         1024       262153
We can modify its retention_type as NONE by:

alter table TEST_TAB_LOB modify lob(mylob) (retention none);

SQL > select t.column_name, t.retention_type, retention_value, s.segment_name, s.tablespace_name, s.header_file, s.relative_fno, s.header_block--, t.*
        from dba_lobs t, dba_segments s where t.owner='K' and t.table_name='TEST_TAB_LOB' and t.segment_name = s.segment_name;

  COLUMN_NAME  RETENTION_TYPE  RETENTION_VALUE SEGMENT_NAME               TABLESPACE_NAME   HEADER_FILE RELATIVE_FNO HEADER_BLOCK
  ------------ --------------- --------------- -------------------------- ----------------- ----------- ------------ ------------
  MYLOB        NONE                            SYS_LOB0005743559C00005$$  TEST_TBS                 1625         1024       262153
and then repeat above test:

SQL > exec test_lob_proc(1, 1000, 1e4);

  Each mylob Length = 1056000, SUM Size (MB) =10000
  SUM Size (MB) =10000, Real Contained Size (MB, due to TEST_SEQ_1000.Max Limit) = 1000
  
  Elapsed: 00:08:02.12

SQL > exec check_space_securefile_2('K', 'TEST_TAB_LOB', 'MYLOB');

  ===========================================================================
  segment_size_blocks = 163960
  used_blocks         = 144372 (88.05 %)
  expired_blocks      = 19588 (11.95 %)
  unexpired_blocks    = 0 (0 %)
  ===========================================================================
  Segment Blocks/Bytes   = 163960 / 1343160320 (1280.94 MB)
  Unused Blocks/Bytes    = 0 / 0 (0 %)
  Used Blocks/Bytes      = 144372 / 1182695424 (88.05 %)
  Expired Blocks/Bytes   = 19588 / 160464896 (11.95 %)
  Unexpired Blocks/Bytes = 0 / 0 (0 %)
  ===========================================================================
  NON Data Blocks  = 19588 (11.95 %)
  NON_data_blocks_2 (= segment_size_blocks - used_blocks) = 19588
  ===========================================================================
  LOBSEGMENT DBA_EXTENTS storage size Blocks  = 163960, CNT = 140, MIN_Blocks = 120, MAX_Blocks = 8192
  LOBSEGMENT DBA_EXTENTS storage size Byets   = 1343160320 (1280.94 MB)
  LOBSEGMENT DBA_SEGMENTS storage size Blocks = 163960
  LOBSEGMENT DBA_SEGMENTS storage size Byets  = 1343160320 (1280.94 MB)

SQL > select round(sum(dbms_lob.getlength(mylob))/1024/1024, 2) LOB_MB, round(sum(length(mylob))/1024/1024, 2) MB
            ,count(id), min(id), max(id), min(ts), max(ts) from test_tab_lob;

   LOB_MB         MB  COUNT(ID)    MIN(ID)    MAX(ID) MIN(TS)               MAX(TS)
  ------- ---------- ---------- ---------- ---------- --------------------- --------------------
  1007.08    1007.08       1000          1       1000 17-FEB-2025 11:41:04  17-FEB-2025 11:41:52
The above output shows that when RETENTION_TYPE=NONE, only 1280.94 MB is used and expired_blocks is 11.95%, compared to previous RETENTION_TYPE=DEFAULT, 10170.94 MB is used and expired_blocks is 88.81%.

Strangely, once RETENTION_TYPE is set to NONE, setting back to DEFAULT gets error:

SQL > alter table TEST_TAB_LOB modify lob(mylob) (retention default);
  alter table TEST_TAB_LOB modify lob(mylob) (retention default)
                                                      *
  ERROR at line 1:
  ORA-22853: invalid LOB storage option specification
Here is one hacking workaround to set it back to DEFAULT:

select header_file, relative_fno, header_block, t.* from dba_segments t where segment_name = 'SYS_LOB0005742851C00004$$';
  --1625	137
                   
select lists
      ,decode(s.lists, 0, 'NONE', 1, 'AUTO',
                       2, 'MIN',  3, 'MAX',
                       4, 'DEFAULT', 'INVALID') ora_retention_type
      ,(select retention_type from  dba_lobs t where table_name='TEST_TAB_LOB') dba_retention_type
      ,s.* 
from sys.seg$ s where file# = 1024 and block# = 262153;   -- lists stores RETENTION

  --4	DEFAULT	DEFAULT	1024	262153
  --0	NONE	  NONE	  1024	262153

update sys.seg$ set lists= 4  where file# = 1024 and block# = 262153;   -- set RETENTION_TYPE to DEFAULT as DBA
commit;
In this Blog, we tested CLOB with "SECUREFILE/COMPRESS/NOCACHE", the similar behavior is also observed with "BASICFILE/NO COMPRESS/CACHE READS".


2. Test Setup



drop tablespace test_tbs including contents and datafiles;

create bigfile tablespace test_tbs datafile '/oratestdb/oradata/testdb/test_tbs.dbf' size 10G online;

drop table test_tab_lob cascade constraints;

create table test_tab_lob(id number, cnt number, ts timestamp with local time zone default localtimestamp, lob_state varchar2(20), mylob clob) 
  segment creation immediate tablespace test_tbs
  lob (mylob) store as securefile (
   tablespace test_tbs enable storage in row chunk 8192 retention 
   nocache logging compress medium keep_duplicates
   storage(initial 65536 next 131072 minextents 1 maxextents 2147483645
   pctincrease 0 freelists 1 freelist groups 1
   buffer_pool default flash_cache default cell_flash_cache default))  enable row movement;

drop sequence test_seq_1000;

create sequence test_seq_1000 start with 1 maxvalue 1000 minvalue 1 cycle cache 10 global;


3. Test Code



create or replace procedure test_lob_proc(p_job_nr number, p_kb_cnt number, p_loop_cnt number) as 
  l_mylob     clob;
  l_src_mylob clob;
  l_lob_32kb  clob := lpad('abc', 32000, 'x');
  l_seq       number;
  l_ts_text   varchar2(50);
begin
  l_ts_text   := to_char(localtimestamp, 'YYYY*MON*DD-HH24:MI:SS.FF9');
  l_lob_32kb := dbms_random.string('p', 4000);
  
  --CLOB stored in AL16UTF16 (wo bytes) in AL32UTF8 database. Use dbms_random.string due to CLOB compress
  for i in 1..7 loop
    dbms_lob.append(dest_lob => l_lob_32kb, src_lob => dbms_random.string('p', 4000));  
  end loop;
  l_src_mylob := l_lob_32kb;
  if p_kb_cnt >= 32 then
	  for i in 1..round(p_kb_cnt/32)+1 loop
	    dbms_lob.append(l_src_mylob, l_lob_32kb);
	  end loop;
	else
	  l_src_mylob := dbms_lob.substr(l_lob_32kb, p_kb_cnt*1000, 1);
	end if;
  dbms_output.put_line('Each mylob Length = '||dbms_lob.getlength(l_src_mylob) ||', SUM Size (MB) ='|| (p_loop_cnt*p_kb_cnt/1000));
  dbms_output.put_line('SUM Size (MB) ='|| (p_loop_cnt*p_kb_cnt/1000) ||', Real Contained Size (MB, due to TEST_SEQ_1000.Max Limit) = '|| (1000*p_kb_cnt/1000));
  
  for k in 1..p_loop_cnt loop
	  l_seq := test_seq_1000.nextval;

		merge 
		  into  test_tab_lob l
		  using (select l_seq id, 1 cnt, localtimestamp ts, 'pending' lob_state, empty_clob() mylob from dual) v
		  on (l.id = v.id)
		  when matched then
		    update set cnt = v.cnt + 1, mylob = v.mylob
		  when not matched then
		    insert (id, cnt, ts, mylob) values (l_seq, 1, localtimestamp, v.mylob);
		
		--select mylob into l_mylob from test_tab_lob where id = l_seq;
		--dbms_lob.append(dest_lob => l_mylob, src_lob => l_src_mylob);
    
    -- @TODO First commit more important table scalar columns (fast by LGWR and DBWR) to make them visible, and then mise-a-jour LOB column.
    commit;
    update test_tab_lob set lob_state = 'updated', mylob = l_src_mylob where id = l_seq;
    
	  commit;
	  
	  if k <= 10 then 
		  dbms_output.put_line('Create id = '||l_seq||', mylob Length = '||dbms_lob.getlength(l_src_mylob));
		end if;
    if k = 10 then 
		  dbms_output.put_line('Create id: ........ only print first 10 id');
		end if;
	end loop;
end;
/

-- exec test_lob_proc(1, 1000*3, 10);     -- insert 10 rows with mylob length=3MB 

-------------------- create test jobs ----------------------

create or replace procedure test_lob_proc_job(p_job_cnt number, p_kb_cnt number, p_loop_cnt number) as
  l_job_name varchar2(50);
begin
  for i in 1..p_job_cnt loop
    l_job_name := 'TEST_JOB_ENQ_'||i;
    dbms_scheduler.create_job (
      job_name        => l_job_name,
      job_type        => 'PLSQL_BLOCK',
      job_action      => 
        'begin 
           test_lob_proc('||i||', '||p_kb_cnt||', '||p_loop_cnt||');
        end;',    
      start_date      => systimestamp,
      --repeat_interval => 'systimestamp',
      auto_drop       => true,
      enabled         => true);
  end loop;
end;
/


4. LOB Space Usage



------------------------------------ SECUREFILE LOB space usage ------------------------------------
--   MOS: How to Determine what storage is used in a LOBSEGMENT and should it be shrunk / reorganized? (Doc ID 1453350.1)

CREATE OR REPLACE PROCEDURE check_space_securefile (u_name in varchar2, v_segname varchar2) IS
  l_segment_size_blocks NUMBER;
  l_segment_size_bytes NUMBER;
  l_used_blocks NUMBER;
  l_used_bytes NUMBER;
  l_expired_blocks NUMBER;
  l_expired_bytes NUMBER;
  l_unexpired_blocks NUMBER;
  l_unexpired_bytes NUMBER;
  l_unused_blocks NUMBER;
  l_unused_bytes NUMBER;
  l_non_data_blocks NUMBER;
  l_non_data_bytes NUMBER;
  l_non_data_blocks_2 NUMBER;
  BEGIN
    DBMS_SPACE.SPACE_USAGE( segment_owner =>u_name,                        --segment_owner           IN    VARCHAR2,                 
                            segment_name => v_segname,                     --segment_name            IN    VARCHAR2,              
                            segment_type => 'LOB',                         --segment_type            IN    VARCHAR2,      
                            suoption     => DBMS_SPACE.SPACEUSAGE_EXACT,   -- or DBMS_SPACE.SPACEUSAGE_FAST
                            segment_size_blocks => l_segment_size_blocks,  --segment_size_blocks     OUT   NUMBER,                
                            segment_size_bytes => l_segment_size_bytes,    --segment_size_bytes      OUT   NUMBER,                
                            used_blocks => l_used_blocks,                  --used_blocks             OUT   NUMBER,                
                            used_bytes => l_used_bytes,                    --used_bytes              OUT   NUMBER,                
                            expired_blocks => l_expired_blocks,            --expired_blocks          OUT   NUMBER,                
                            expired_bytes => l_expired_bytes,              --expired_bytes           OUT   NUMBER,                
                            unexpired_blocks => l_unexpired_blocks,        --unexpired_blocks        OUT   NUMBER,                
                            unexpired_bytes => l_unexpired_bytes           --unexpired_bytes         OUT   NUMBER,                
                                                                           --partition_name          IN    VARCHAR2 DEFAULT NULL
                           );
    DBMS_OUTPUT.PUT_LINE('===========================================================================');
    DBMS_OUTPUT.PUT_LINE('segment_size_blocks = '||l_segment_size_blocks);
    DBMS_OUTPUT.PUT_LINE('used_blocks         = '||l_used_blocks     ||' ('||(round(100*l_used_blocks/l_segment_size_blocks, 2))||' %)');
    DBMS_OUTPUT.PUT_LINE('expired_blocks      = '||l_expired_blocks  ||' ('||(round(100*l_expired_blocks/l_segment_size_blocks, 2))||' %)');
    DBMS_OUTPUT.PUT_LINE('unexpired_blocks    = '||l_unexpired_blocks||' ('||(round(100*l_unexpired_blocks/l_segment_size_blocks, 2))||' %)');
    DBMS_OUTPUT.PUT_LINE('===========================================================================');
    
    l_unused_blocks := l_segment_size_blocks - (l_used_blocks + l_expired_blocks + l_unexpired_blocks);
    l_unused_bytes  := l_segment_size_bytes - (l_used_bytes + l_expired_bytes + l_unexpired_bytes);

    l_non_data_blocks   := l_unused_blocks + l_expired_blocks + l_unexpired_blocks;
    l_non_data_blocks_2 := l_segment_size_blocks - l_used_blocks;
    
    DBMS_OUTPUT.ENABLE;
    DBMS_OUTPUT.PUT_LINE('Segment Blocks/Bytes   = '||l_segment_size_blocks||' / '||l_segment_size_bytes||' ('||round(l_segment_size_bytes/1024/1024, 2) ||' MB)');
    DBMS_OUTPUT.PUT_LINE('Unused Blocks/Bytes    = '||l_unused_blocks      ||' / '||l_unused_bytes      ||' ('||(round(100*l_unused_blocks/l_segment_size_blocks, 2))||' %)');
    DBMS_OUTPUT.PUT_LINE('Used Blocks/Bytes      = '||l_used_blocks        ||' / '||l_used_bytes        ||' ('||(round(100*l_used_blocks/l_segment_size_blocks, 2))||' %)');
    DBMS_OUTPUT.PUT_LINE('Expired Blocks/Bytes   = '||l_expired_blocks     ||' / '||l_expired_bytes     ||' ('||(round(100*l_expired_blocks/l_segment_size_blocks, 2))||' %)');
    DBMS_OUTPUT.PUT_LINE('Unexpired Blocks/Bytes = '||l_unexpired_blocks   ||' / '||l_unexpired_bytes   ||' ('||(round(100*l_unexpired_blocks/l_segment_size_blocks, 2))||' %)');
    DBMS_OUTPUT.PUT_LINE('===========================================================================');
    DBMS_OUTPUT.PUT_LINE('NON Data Blocks  = '||l_non_data_blocks||' ('||(round(100*l_non_data_blocks/l_segment_size_blocks, 2))||' %)');
    DBMS_OUTPUT.PUT_LINE('NON_data_blocks_2 (= segment_size_blocks - used_blocks) = '||l_non_data_blocks_2);
    DBMS_OUTPUT.PUT_LINE('===========================================================================');
    
    -- Determine the storage size of the LOBSEGMENT
    for c in (SELECT round(SUM(BYTES)/8192) blocks, SUM(BYTES) bytes, count(*) cnt, min(blocks) MIN_Blocks, max(blocks) MAX_Blocks FROM DBA_EXTENTS WHERE SEGMENT_NAME = v_segname) loop
      DBMS_OUTPUT.PUT_LINE('LOBSEGMENT DBA_EXTENTS storage size Blocks  = '||c.blocks||', CNT = ' ||c.cnt  ||', MIN_Blocks = '||c.MIN_Blocks ||', MAX_Blocks = '||c.MAX_Blocks);
      DBMS_OUTPUT.PUT_LINE('LOBSEGMENT DBA_EXTENTS storage size Byets   = '||c.bytes ||' ('||round(c.bytes/1024/1024, 2) ||' MB)');
    end loop;
    
    for c in (SELECT round(SUM(BYTES)/8192) blocks, SUM(BYTES) bytes FROM DBA_SEGMENTS WHERE SEGMENT_NAME = v_segname) loop
      DBMS_OUTPUT.PUT_LINE('LOBSEGMENT DBA_SEGMENTS storage size Blocks = '||c.blocks);
      DBMS_OUTPUT.PUT_LINE('LOBSEGMENT DBA_SEGMENTS storage size Byets  = '||c.bytes ||' ('||round(c.bytes/1024/1024, 2) ||' MB)');
    end loop;
  END;
/

CREATE OR REPLACE PROCEDURE check_space_securefile_2 (v_owner_name in varchar2, v_table_name varchar2, v_column_name varchar2) IS
  l_lob_segment_name varchar2(100);
begin
  select segment_name into l_lob_segment_name from dba_lobs where owner=v_owner_name and table_name=v_table_name and column_name=v_column_name;
  check_space_securefile(u_name=>v_owner_name, v_segname=>l_lob_segment_name);
end;
/