Oracle Scratchpad

February 1, 2023

Lob Space redux

Filed under: Infrastructure,LOBs,Oracle,unfinished — Jonathan Lewis @ 1:56 pm GMT Feb 1,2023

At present this is just a place holder to remind me to finish commenting on (and correcting) a mistake I made when I wrote a note about the number of bytes of data you could get into an “enable storage in row” LOB before it had to be stored out of row.

In a much earlier article discussing multi-byte, variable length character sets I showed that CLOBs stored in-row are likely to take far more space than the equivalent varchar2() columns, and that securefile CLOBs would use 6 bytes less than Basicfile CLOBs. These claims are (or were) true.

In another article I then discussed the number of bytes at which a LOB that was defined as “enable storage in row” would go “out of row” and showed some results that appeared to demonstrate that the length varied with version of Oracle. This was wrong. In my testing I had not dumped any datablocks to check their actual content, instead I had relied on the (undocumented) sys_op_opnsize() function and been fooled by the results.

This note is intended to correct my error. I’ll start with a test on a BLOB, enabling storage in row, and create some data that will be close to the breakpoint for the value going out of row:

rem
rem     Script:         blob_in_row.sql
rem     Author:         Jonathan Lewis
rem     Dated:          Jan 2023
rem
rem     Last tested 
rem             19.11.0.0
rem             11.2.0.4
rem 

create table t1b (
        id      number(3,0),
        pad     varchar2(2000),
        b1      blob
)
lob(b1) store as
        basicfile
        basblob (
                enable storage in row
        )
/

create table t1s (
        id      number(3,0),
        padding varchar2(2000),
        b1      blob
)
lob(b1) store as
        securefile
        secblob (
                enable storage in row
        )
/

alter table t1b add constraint t1b_pk primary key (id);
alter table t1s add constraint t1s_pk primary key (id);

declare
        raw_source      raw(4200);
begin
        for i in 1..15 loop
                raw_source := rpad('E',2 * 3960 + 2*i,'E');
                insert into t1b values(i,rpad(' ',1500),raw_source);
                insert into t1s values(i,rpad(' ',1500),raw_source);
                commit;
        end loop;
end;
/

select
        id, 
        dbms_rowid.rowid_relative_fno(rowid)    rel_file_no,
        dbms_rowid.rowid_block_number(rowid)    block_no,
        sys_op_opnsize(b1),
        dbms_lob.getlength(b1)
from
        t1b
order by
        id
/

select
        id, 
        dbms_rowid.rowid_relative_fno(rowid)    rel_file_no,
        dbms_rowid.rowid_block_number(rowid)    block_no,
        sys_op_opnsize(b1),
        dbms_lob.getlength(b1)
from
        t1s
order by
        id
/

I’ve created two tables, one to hold a basicfile blob and one to hold a securefile blob, then I’ve run a little PL/SQL loop to insert rows into the table where the first row holds a blob of 3961bytes and each row increases the length of the blob by 1. I’ve started with a string ‘E’s of twice the required length and allowed Oracle to do an implicit conversion (hextoraw) to bytes.

To avoid anomalies and hassle from chained rows etc. I’ve included a column of varchar2(1500) so that there’s no question of Oracle being able to squeeze partial rows into blocks.

After I’ve done this I’ve executed a pair of queries that report for each row the block it starts in, its apparent length according to the sys_op_opnsize(), and its length according o the dbms_lob.getlength() function.

Here are the results under 11.2.0.4, first the basicfile table, then the securefile table:

11.2.0.4

        ID REL_FILE_NO   BLOCK_NO SYS_OP_OPNSIZE(B1) DBMS_LOB.GETLENGTH(B1)
---------- ----------- ---------- ------------------ ----------------------
         1           5        199                 86                   3961
         2           5        195                 86                   3962
         3           5        196                 86                   3963
         4           5        197                 86                   3964
         5           5        197                106                   3965
         6           5        198                106                   3966
         7           5        198                106                   3967
         8           5        198                106                   3968
         9           5        198                106                   3969
        10           5      22560                106                   3970
        11           5      22560                106                   3971
        12           5      22560                106                   3972
        13           5      22560                106                   3973
        14           5      22564                106                   3974
        15           5      22564                106                   3975

        ID REL_FILE_NO   BLOCK_NO SYS_OP_OPNSIZE(B1) DBMS_LOB.GETLENGTH(B1)
---------- ----------- ---------- ------------------ ----------------------
         1           5        231                 86                   3961
         2           5        227                 86                   3962
         3           5        228                 86                   3963
         4           5        229                 86                   3964
         5           5        230                 86                   3965
         6           5      22536                 86                   3966
         7           5      22540                 86                   3967
         8           5      22541                 86                   3968
         9           5      22541                 86                   3969
        10           5      22542                 86                   3970
        11           5      22542                 86                   3971
        12           5      22542                 86                   3972
        13           5      22542                 86                   3973
        14           5      22543                 86                   3974
        15           5      22543                 86                   3975

You can see that in both cases the sys_op_opnsize() call reports only a few dozen bytes even though we know the length of the BLOB is always more than 3,960 bytes. My original mistake was to assume that this very small number appeared when the BLOB went out of row and was reporting the length of the LOB locator that had been left behind in the row.

This time I’ve going to dump blocks; and a good target block to dump is the first block in each result set that holds two rows: block (5,197) for the basicfile BLOB, and block (5,22541) for the securefile BLOB. (Interestingly the basicfile BLOB shows the sys_op_opnsize() value change in the second of the two rows in (5,197) – is that a coincidence?)

Basicfile block dump (extract)

tab 0, row 0, @0xa10
tl: 5512 fb: --H-FL-- lb: 0x0  cc: 3
col  0: [ 2]  c1 05
col  1: [1500]
 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
...
col  2: [4000]
 00 54 00 01 01 0c 00 00 00 01 00 00 00 01 00 00 10 de 99 8d 0f 8c 09 00 00
 00 00 00 0f 7c 00 00 00 00 00 01 ee ee ee ee ee ee ee ee ee ee ee ee ee ee
...
LOB
Locator:
  Length:        84(4000)
  Version:        1
  Byte Length:    1
  LobID: 00.00.00.01.00.00.10.de.99.8d
  Flags[ 0x01 0x0c 0x00 0x00 ]:
    Type: BLOB
    Storage: BasicFile
    Enable Storage in Row
    Characterset Format: IMPLICIT
    Partitioned Table: No
    Options: ReadWrite
  Inode:
    Size:     3980
    Flag:     0x09 [ Valid DataInRow ]
    Future:   0x00 (should be '0x00')
    Blocks:   0
    Bytes:    3964
    Version:  00000.0000000001
    Inline data[3964]

...

tab 0, row 1, @0x402
tl: 1550 fb: --H-FL-- lb: 0x0  cc: 3
col  0: [ 2]  c1 06
col  1: [1500]
 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
 ...
col  2: [40]
 00 54 00 01 01 0c 00 00 00 01 00 00 00 01 00 00 10 de 99 8f 00 14 05 00 00
 00 00 00 0f 7d 00 00 00 00 00 02 01 40 00 d7
LOB
Locator:
  Length:        84(40)
  Version:        1
  Byte Length:    1
  LobID: 00.00.00.01.00.00.10.de.99.8f
  Flags[ 0x01 0x0c 0x00 0x00 ]:
    Type: BLOB
    Storage: BasicFile
    Enable Storage in Row
    Characterset Format: IMPLICIT
    Partitioned Table: No
    Options: ReadWrite
  Inode:
    Size:     20
    Flag:     0x05 [ Valid InodeInRow(ESIR) ]
    Future:   0x00 (should be '0x00')
    Blocks:   0
    Bytes:    3965
    Version:  00000.0000000002
    DBA Array[1]:
      0x014000d7
end_of_block_dump


At line 7 you can see that the BLOB column for the first row is reporting 4,000 bytes, although line 29 tells us that the data content is only 3,964 of those bytes and line 24 tells us we have “Valid data in row”.

At line 41 we can see that the BLOB column for the second row holds only 40 bytes, line 59 tells use we have a “Valid inode in row”, and possibly the “Size: 20” at line 58 explains why the sys_op_opnsize() changed from 86 to 106 at this point. You’ll notice at line 62 that this is the BLOB where the actual data size is just one more byte than the previous row at 3,965 bytes.

Securefile block dump (extract)

tab 0, row 0, @0xa11
tl: 5511 fb: --H-FL-- lb: 0x0  cc: 3
col  0: [ 2]  c1 09
col  1: [1500]
 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
 ...
col  2: [3999]
 00 54 00 01 01 0c 00 80 00 01 00 00 00 01 00 00 10 de 99 96 0f 8b 48 90 0f
 85 01 00 0f 80 01 ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee
 ...
LOB 
Locator:
  Length:        84(3999)
  Version:        1
  Byte Length:    1
  LobID: 00.00.00.01.00.00.10.de.99.96
  Flags[ 0x01 0x0c 0x00 0x80 ]:
    Type: BLOB 
    Storage: SecureFile
    Characterset Format: IMPLICIT
    Partitioned Table: No
    Options: ReadWrite
  SecureFile Header:
    Length:   3979
    Old Flag: 0x48 [ DataInRow SecureFile ]
    Flag 0:   0x90 [ INODE Valid ]
    Layers:
      Lengths Array: INODE:3973
        01 00 0f 80 01 ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee  ...

tab 0, row 1, @0x405
tl: 1548 fb: --H-FL-- lb: 0x0  cc: 3
col  0: [ 2]  c1 0a
col  1: [1500]
 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
...
col  2: [38]
 00 54 00 01 01 0c 00 80 00 01 00 00 00 01 00 00 10 de 99 98 00 12 40 90 00
 0c 21 00 0f 81 01 00 01 01 40 58 11 01
LOB
Locator:
  Length:        84(38)
  Version:        1
  Byte Length:    1
  LobID: 00.00.00.01.00.00.10.de.99.98
  Flags[ 0x01 0x0c 0x00 0x80 ]:
    Type: BLOB
    Storage: SecureFile
    Characterset Format: IMPLICIT
    Partitioned Table: No
    Options: ReadWrite
  SecureFile Header:
    Length:   18
    Old Flag: 0x40 [ SecureFile ]
    Flag 0:   0x90 [ INODE Valid ]
    Layers:
      Lengths Array: INODE:12
      INODE:
        21 00 0f 81 01 00 01 01 40 58 11 01

At line 7 we can see the BLOB column for the first row in the block has a length of 3,999 bytes but line 37 tells use that the next row – with one extra byte – reports a BLOB column holding only 38 bytes. We know from our code that the 3,999 bytes was 3,968 bytes of data, with 31 bytes for various fragments LOB overhead. So there is a small difference in size between basicfile and securefile BLOBs before BLOBs declared as “enable storage in row” are forced out of the row. That’s in 11g, of course, but is it the same in 19c?

19.11.0.0

        ID REL_FILE_NO   BLOCK_NO SYS_OP_OPNSIZE(B1) DBMS_LOB.GETLENGTH(B1)
---------- ----------- ---------- ------------------ ----------------------
         1          36       1672                114                   3961
         2          36       1676                114                   3962
         3          36       1680                114                   3963
         4          36       1684                114                   3964
         5          36       1684                134                   3965
         6          36       1688                134                   3966
         7          36       1688                134                   3967
         8          36       1688                134                   3968
 ...
       ID REL_FILE_NO   BLOCK_NO SYS_OP_OPNSIZE(B1) DBMS_LOB.GETLENGTH(B1)
---------- ----------- ---------- ------------------ ----------------------
         1          36       2184                114                   3961
         2          36       2188                114                   3962
         3          36       2192                114                   3963
         4          36       2196                114                   3964
         5          36       2200                114                   3965
         6          36       2204                114                   3966
         7          36       2208                114                   3967
         8          36       2212                114                   3968
         9          36       2212                132                   3969
        10          36       2216                132                   3970

Again I’ve reported the basicfile results before the securefile results. The numbers we get from sys_op_opnsize() don’t agree with the 11g results, but again we see from the dbms_lob.getlength() numbers that the first point at which we get two consecutive rows into a block is 3,964 bytes of data for the basicfile BLOB (36, 1684) and 3968 bytes for the securefile BLOB (36,2212). In this case the sys_op_opnsize() value changes on the second row of the block for both basicfile and securefile.

Of course I need to dump blocks again – just to be sure – but this time I’m going to keep the extract very short – just a few bytes of each column for the two rows in each block:

Basicfile blockdump (extract)

tab 0, row 0, @0xa10
tl: 5512 fb: --H-FL-- lb: 0x0  cc: 3
col  0: [ 2]  c1 05
col  1: [1500]
 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
...
col  2: [4000]
 00 70 00 01 01 0c 00 00 00 01 00 00 00 01 00 00 0c 55 e4 28 0f 8c 09 00 00
 00 00 00 0f 7c 00 00 00 00 00 01 ee ee ee ee ee ee ee ee ee ee ee ee ee ee

tab 0, row 1, @0x402
tl: 1550 fb: --H-FL-- lb: 0x0  cc: 3
col  0: [ 2]  c1 06
col  1: [1500]
 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
...
col  2: [40]
 00 70 00 01 01 0c 00 00 00 01 00 00 00 01 00 00 0c 55 e4 2a 00 14 05 00 00
 00 00 00 0f 7d 00 00 00 00 00 02 09 00 07 88

Securefile blockdump (extract)

tab 0, row 0, @0xa11
tl: 5511 fb: --H-FL-- lb: 0x0  cc: 3
col  0: [ 2]  c1 09
col  1: [1500]
 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
...
col  2: [3999]
 00 70 00 01 01 0c 00 80 00 01 00 00 00 01 00 00 0c 55 e4 31 0f 8b 48 90 0f
 85 01 00 0f 80 01 ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee

tab 0, row 1, @0x405
tl: 1548 fb: --H-FL-- lb: 0x0  cc: 3
col  0: [ 2]  c1 0a
col  1: [1500]
 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
...
col  2: [38]
 00 70 00 01 01 0c 00 80 00 01 00 00 00 01 00 00 0c 55 e4 33 00 12 40 90 00
 0c 21 00 0f 81 01 00 01 09 00 09 c1 01

The symbolic dumps of LOBs has disappeared in the later versions of Oracle, but it’s quite easy from the raw stream of bytes that the raw dumps for 19c match the dumps for11g, and the break points from in-row to out of row are the same in the two versions – it’s just the sys_op_opnsize() function calls that show inconsistent (and somewhat strange) behaviour.

Summary

This note walks through an examination of “in-row” BLOBs to find out how large the data can get before the BLOB is forced out of row.

We find that the breakpoint for securefile BLOBs is a few bytes larger than it is for basicfile BLOBs, 3,968 bytes compared to 3,964 bytes. These sizes, and this difference is consistent between 11.2.0.4 and 19.11.0.0.

There does seem to be a tiny difference securefiles and basicfiles: LOB overhead + raw data = 4,000 for basicfiles while it is 3,999 for securefiles.

Repeating the tests for difference character sets (single byte, multibyte fixed, and multibyte varying) is left as an exercise to the reader.

2 Comments »

  1. […] exceeds 4,000 bytes – which means (in many cases) when the original string content exceeds something between 1,938 and roughly 1,985 characters, depending on your version of Oracle and whether you are using basicfile […]

    Pingback by In-row LOBs | Oracle Scratchpad — February 6, 2023 @ 10:24 am GMT Feb 6,2023 | Reply

  2. […] not a safe indication of whether the lob is still in-row or not. I am currently (still) working on a note showing corrected information, while also correcting related blog […]

    Pingback by LOB length | Oracle Scratchpad — February 6, 2023 @ 12:48 pm GMT Feb 6,2023 | Reply


RSS feed for comments on this post. TrackBack URI

Comments and related questions are welcome.

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Website Powered by WordPress.com.

%d bloggers like this: