Skip to content

DBMS_LOB - Read, Write & Manage Oracle LOB Data

DBMS_LOB - Read, Write & Manage Oracle LOB Data

Section titled “DBMS_LOB - Read, Write & Manage Oracle LOB Data”

DBMS_LOB is Oracle’s built-in package for working with Large Objects (LOBs). It provides procedures and functions to read, write, append, copy, and manipulate CLOB, NCLOB, BLOB, and BFILE data types. LOBs can store up to 128 TB of unstructured data, making DBMS_LOB essential for any application handling documents, images, XML, or large text.

TypeDescriptionMax Size
CLOBCharacter Large Object (database character set)128 TB
NCLOBNational Character Large Object (Unicode)128 TB
BLOBBinary Large Object (raw bytes)128 TB
BFILEBinary file stored on OS filesystem (read-only)OS limit

Oracle 11g introduced SecureFile LOBs as the modern storage format, replacing the older BasicFile format. Always prefer SecureFile for new tables.

-- Create table with SecureFile CLOB (recommended, Oracle 11g+)
CREATE TABLE documents (
doc_id NUMBER PRIMARY KEY,
doc_name VARCHAR2(200),
doc_text CLOB,
doc_data BLOB
)
LOB (doc_text) STORE AS SECUREFILE (
TABLESPACE lob_data
ENABLE STORAGE IN ROW
CHUNK 8192
CACHE
COMPRESS HIGH
)
LOB (doc_data) STORE AS SECUREFILE (
TABLESPACE lob_data
NOCACHE
NOLOGGING
);
-- Check LOB storage type for existing tables
SELECT
t.table_name,
l.column_name,
l.securefile,
l.in_row,
l.chunk,
l.cache,
l.logging,
l.compression,
l.deduplication
FROM dba_lobs l
JOIN dba_tables t ON l.table_name = t.table_name AND l.owner = t.owner
WHERE l.owner = 'SCOTT'
ORDER BY t.table_name, l.column_name;

Returns the length of a LOB value in bytes (BLOB/BFILE) or characters (CLOB/NCLOB).

-- Syntax
DBMS_LOB.GETLENGTH(lob_loc IN BLOB|CLOB|BFILE) RETURN INTEGER;
-- Example: check document sizes
SELECT
doc_id,
doc_name,
DBMS_LOB.GETLENGTH(doc_text) AS clob_chars,
DBMS_LOB.GETLENGTH(doc_data) AS blob_bytes
FROM documents
WHERE DBMS_LOB.GETLENGTH(doc_text) > 0
ORDER BY DBMS_LOB.GETLENGTH(doc_text) DESC;

Extracts a substring from a CLOB. Equivalent to SQL SUBSTR but works on full LOB content beyond 32767 characters.

-- Syntax
DBMS_LOB.SUBSTR(
lob_loc IN CLOB,
amount IN INTEGER := 32767, -- characters to read
offset IN INTEGER := 1 -- starting position
) RETURN VARCHAR2;
-- Extract first 200 characters as a preview
SELECT
doc_id,
doc_name,
DBMS_LOB.SUBSTR(doc_text, 200, 1) AS preview
FROM documents;
-- Find content starting at a known position
SELECT DBMS_LOB.SUBSTR(doc_text, 500, 10001) AS mid_section
FROM documents
WHERE doc_id = 42;

Reads a portion of LOB data into a buffer. Used in procedural code for chunked processing.

-- Syntax
DBMS_LOB.READ(
lob_loc IN BLOB|CLOB|BFILE,
amount IN OUT NOCOPY BINARY_INTEGER, -- bytes/chars requested; updated to actual
offset IN INTEGER, -- 1-based starting position
buffer OUT NOCOPY RAW|VARCHAR2
);
-- Read a CLOB in a single call (up to 32767 chars)
DECLARE
l_clob CLOB;
l_buffer VARCHAR2(32767);
l_amount BINARY_INTEGER := 32767;
l_offset INTEGER := 1;
BEGIN
SELECT doc_text INTO l_clob FROM documents WHERE doc_id = 1;
DBMS_LOB.READ(l_clob, l_amount, l_offset, l_buffer);
DBMS_OUTPUT.PUT_LINE('Read ' || l_amount || ' characters.');
DBMS_OUTPUT.PUT_LINE(SUBSTR(l_buffer, 1, 100));
END;
/

Writes data into an existing LOB at a specified offset. The LOB must already exist (use EMPTY_CLOB() or EMPTY_BLOB() as a placeholder).

-- Syntax
DBMS_LOB.WRITE(
lob_loc IN OUT NOCOPY BLOB|CLOB,
amount IN BINARY_INTEGER, -- bytes/chars to write
offset IN INTEGER, -- 1-based position
buffer IN RAW|VARCHAR2
);
-- Insert a row with empty LOB, then write content
DECLARE
l_clob CLOB;
BEGIN
-- Step 1: Insert placeholder and lock row
INSERT INTO documents (doc_id, doc_name, doc_text)
VALUES (100, 'Sample Document', EMPTY_CLOB())
RETURNING doc_text INTO l_clob;
-- Step 2: Write content to the LOB locator
DBMS_LOB.WRITE(l_clob, LENGTH('Hello, Oracle LOB!'), 1, 'Hello, Oracle LOB!');
COMMIT;
DBMS_OUTPUT.PUT_LINE('Written: ' || DBMS_LOB.GETLENGTH(l_clob) || ' chars');
END;
/

Appends the content of one LOB to another. Efficient for building LOBs incrementally.

-- Syntax
DBMS_LOB.APPEND(
dest_lob IN OUT NOCOPY BLOB|CLOB,
src_lob IN BLOB|CLOB
);
-- Append one document's text to another
DECLARE
l_dest CLOB;
l_src CLOB;
BEGIN
SELECT doc_text INTO l_dest FROM documents WHERE doc_id = 1 FOR UPDATE;
SELECT doc_text INTO l_src FROM documents WHERE doc_id = 2;
DBMS_LOB.APPEND(l_dest, l_src);
COMMIT;
DBMS_OUTPUT.PUT_LINE('New length: ' || DBMS_LOB.GETLENGTH(l_dest));
END;
/
-- Append a string literal to a CLOB using WRITEAPPEND
DECLARE
l_clob CLOB;
l_text VARCHAR2(200) := CHR(10) || 'Appended line at: ' || TO_CHAR(SYSDATE);
BEGIN
SELECT doc_text INTO l_clob FROM documents WHERE doc_id = 1 FOR UPDATE;
DBMS_LOB.WRITEAPPEND(l_clob, LENGTH(l_text), l_text);
COMMIT;
END;
/

Truncates a LOB to a specified length.

-- Syntax
DBMS_LOB.TRIM(
lob_loc IN OUT NOCOPY BLOB|CLOB,
newlen IN INTEGER
);
-- Trim a CLOB to first 1000 characters
DECLARE
l_clob CLOB;
BEGIN
SELECT doc_text INTO l_clob FROM documents WHERE doc_id = 1 FOR UPDATE;
DBMS_LOB.TRIM(l_clob, 1000);
COMMIT;
DBMS_OUTPUT.PUT_LINE('Trimmed to: ' || DBMS_LOB.GETLENGTH(l_clob));
END;
/

Copies all or part of one LOB into another at a specified offset.

-- Syntax
DBMS_LOB.COPY(
dest_lob IN OUT NOCOPY BLOB|CLOB,
src_lob IN BLOB|CLOB,
amount IN INTEGER, -- chars/bytes to copy
dest_offset IN INTEGER := 1,
src_offset IN INTEGER := 1
);
-- Copy first 5000 characters of source into destination at position 1001
DECLARE
l_dest CLOB;
l_src CLOB;
BEGIN
SELECT doc_text INTO l_dest FROM documents WHERE doc_id = 10 FOR UPDATE;
SELECT doc_text INTO l_src FROM documents WHERE doc_id = 20;
DBMS_LOB.COPY(l_dest, l_src, 5000, 1001, 1);
COMMIT;
END;
/

Overwrites a portion of a LOB with zero bytes (BLOB) or spaces (CLOB) without changing LOB length.

-- Syntax
DBMS_LOB.ERASE(
lob_loc IN OUT NOCOPY BLOB|CLOB,
amount IN OUT NOCOPY INTEGER, -- bytes/chars to erase
offset IN INTEGER := 1
);
DECLARE
l_clob CLOB;
l_amount INTEGER := 100;
BEGIN
SELECT doc_text INTO l_clob FROM documents WHERE doc_id = 1 FOR UPDATE;
DBMS_LOB.ERASE(l_clob, l_amount, 501); -- Erase chars 501-600
COMMIT;
DBMS_OUTPUT.PUT_LINE('Erased ' || l_amount || ' characters.');
END;
/

Creates a temporary LOB in the temporary tablespace for in-memory processing without a permanent table row.

-- Syntax
DBMS_LOB.CREATETEMPORARY(lob_loc IN OUT NOCOPY BLOB|CLOB,
cache IN BOOLEAN,
dur IN PLS_INTEGER := DBMS_LOB.SESSION);
DBMS_LOB.FREETEMPORARY(lob_loc IN OUT NOCOPY BLOB|CLOB);
-- Build a large string using a temporary CLOB
DECLARE
l_clob CLOB;
l_piece VARCHAR2(32767);
BEGIN
-- Create temp LOB (lives for the session)
DBMS_LOB.CREATETEMPORARY(l_clob, TRUE, DBMS_LOB.SESSION);
-- Build content piece by piece
FOR i IN 1..100 LOOP
l_piece := 'Line ' || LPAD(i, 5, '0') || ': ' ||
RPAD('DATA', 50, 'x') || CHR(10);
DBMS_LOB.WRITEAPPEND(l_clob, LENGTH(l_piece), l_piece);
END LOOP;
DBMS_OUTPUT.PUT_LINE('Temp LOB size: ' || DBMS_LOB.GETLENGTH(l_clob));
-- Use the LOB... then free it
DBMS_LOB.FREETEMPORARY(l_clob);
END;
/

Opening a LOB before multiple read/write operations improves performance by suppressing index and trigger updates during the operation.

-- Open modes: DBMS_LOB.LOB_READONLY, DBMS_LOB.LOB_READWRITE
DECLARE
l_clob CLOB;
BEGIN
SELECT doc_text INTO l_clob FROM documents WHERE doc_id = 1 FOR UPDATE;
-- Open the LOB for readwrite before batch operations
IF DBMS_LOB.ISOPEN(l_clob) = 0 THEN
DBMS_LOB.OPEN(l_clob, DBMS_LOB.LOB_READWRITE);
END IF;
-- Perform multiple write operations
DBMS_LOB.WRITEAPPEND(l_clob, 6, 'line1 ');
DBMS_LOB.WRITEAPPEND(l_clob, 6, 'line2 ');
DBMS_LOB.WRITEAPPEND(l_clob, 6, 'line3 ');
-- Close before committing
DBMS_LOB.CLOSE(l_clob);
COMMIT;
END;
/

Searches a LOB for a pattern and returns the starting position.

-- Syntax
DBMS_LOB.INSTR(
lob_loc IN BLOB|CLOB,
pattern IN RAW|VARCHAR2,
offset IN INTEGER := 1,
nth IN INTEGER := 1 -- find nth occurrence
) RETURN INTEGER;
-- Find the 3rd occurrence of a keyword
DECLARE
l_clob CLOB;
l_pos INTEGER;
BEGIN
SELECT doc_text INTO l_clob FROM documents WHERE doc_id = 1;
l_pos := DBMS_LOB.INSTR(l_clob, 'ERROR', 1, 3);
IF l_pos > 0 THEN
DBMS_OUTPUT.PUT_LINE('3rd ERROR found at position: ' || l_pos);
DBMS_OUTPUT.PUT_LINE('Context: ' || DBMS_LOB.SUBSTR(l_clob, 80, l_pos));
ELSE
DBMS_OUTPUT.PUT_LINE('Pattern not found 3 times.');
END IF;
END;
/

Reading a LOB larger than 32767 characters/bytes requires looping with offsets.

-- Process a CLOB larger than 32767 characters in chunks
DECLARE
l_clob CLOB;
l_buffer VARCHAR2(32767);
l_amount BINARY_INTEGER;
l_offset INTEGER := 1;
l_chunk_size BINARY_INTEGER := 32000;
l_total_len INTEGER;
l_line_count INTEGER := 0;
BEGIN
SELECT doc_text INTO l_clob FROM documents WHERE doc_id = 1;
l_total_len := DBMS_LOB.GETLENGTH(l_clob);
DBMS_OUTPUT.PUT_LINE('Total LOB size: ' || l_total_len || ' chars');
DBMS_LOB.OPEN(l_clob, DBMS_LOB.LOB_READONLY);
WHILE l_offset <= l_total_len LOOP
l_amount := l_chunk_size;
BEGIN
DBMS_LOB.READ(l_clob, l_amount, l_offset, l_buffer);
EXCEPTION
WHEN NO_DATA_FOUND THEN
EXIT;
END;
-- Process this chunk
l_line_count := l_line_count +
LENGTH(l_buffer) - LENGTH(REPLACE(l_buffer, CHR(10), ''));
l_offset := l_offset + l_amount;
END LOOP;
DBMS_LOB.CLOSE(l_clob);
DBMS_OUTPUT.PUT_LINE('Line count: ' || l_line_count);
END;
/
-- Write a large CLOB from VARCHAR2 chunks
CREATE OR REPLACE PROCEDURE write_large_clob (
p_doc_id IN NUMBER,
p_content IN VARCHAR2 -- oversized content passed via wrapper
) AS
l_clob CLOB;
l_chunk VARCHAR2(4000);
l_len INTEGER;
l_pos INTEGER := 1;
l_chunksize CONSTANT INTEGER := 4000;
BEGIN
-- Initialise with empty LOB
INSERT INTO documents (doc_id, doc_name, doc_text)
VALUES (p_doc_id, 'Generated Doc', EMPTY_CLOB())
RETURNING doc_text INTO l_clob;
DBMS_LOB.OPEN(l_clob, DBMS_LOB.LOB_READWRITE);
l_len := LENGTH(p_content);
WHILE l_pos <= l_len LOOP
l_chunk := SUBSTR(p_content, l_pos, l_chunksize);
DBMS_LOB.WRITEAPPEND(l_clob, LENGTH(l_chunk), l_chunk);
l_pos := l_pos + l_chunksize;
END LOOP;
DBMS_LOB.CLOSE(l_clob);
COMMIT;
END write_large_clob;
/

For CLOBs that fit within 32767 characters, a direct cast works. For larger CLOBs, convert in chunks.

-- Safe CLOB to VARCHAR2 for CLOBs <= 32767 chars
CREATE OR REPLACE FUNCTION clob_to_varchar2 (p_clob IN CLOB)
RETURN VARCHAR2
IS
l_len INTEGER;
BEGIN
l_len := DBMS_LOB.GETLENGTH(p_clob);
IF l_len IS NULL OR l_len = 0 THEN
RETURN NULL;
ELSIF l_len <= 32767 THEN
RETURN DBMS_LOB.SUBSTR(p_clob, 32767, 1);
ELSE
-- Return truncated value with indicator
RETURN DBMS_LOB.SUBSTR(p_clob, 32764, 1) || '...';
END IF;
END clob_to_varchar2;
/
-- Usage in a query
SELECT doc_id, clob_to_varchar2(doc_text) AS text_preview
FROM documents
WHERE DBMS_LOB.GETLENGTH(doc_text) <= 32767;
-- Export a BLOB to the filesystem in raw chunks
CREATE OR REPLACE PROCEDURE export_blob_to_file (
p_doc_id IN NUMBER,
p_dir IN VARCHAR2,
p_filename IN VARCHAR2
) AS
l_blob BLOB;
l_file UTL_FILE.FILE_TYPE;
l_buffer RAW(32767);
l_amount BINARY_INTEGER := 32767;
l_offset INTEGER := 1;
l_blob_len INTEGER;
BEGIN
SELECT doc_data INTO l_blob FROM documents WHERE doc_id = p_doc_id;
l_blob_len := DBMS_LOB.GETLENGTH(l_blob);
l_file := UTL_FILE.FOPEN(p_dir, p_filename, 'WB', 32767);
DBMS_LOB.OPEN(l_blob, DBMS_LOB.LOB_READONLY);
WHILE l_offset <= l_blob_len LOOP
l_amount := 32767;
BEGIN
DBMS_LOB.READ(l_blob, l_amount, l_offset, l_buffer);
UTL_FILE.PUT_RAW(l_file, l_buffer, TRUE);
EXCEPTION
WHEN NO_DATA_FOUND THEN EXIT;
END;
l_offset := l_offset + l_amount;
END LOOP;
DBMS_LOB.CLOSE(l_blob);
UTL_FILE.FCLOSE(l_file);
DBMS_OUTPUT.PUT_LINE('Exported ' || l_blob_len || ' bytes to ' || p_filename);
EXCEPTION
WHEN OTHERS THEN
IF UTL_FILE.IS_OPEN(l_file) THEN UTL_FILE.FCLOSE(l_file); END IF;
IF DBMS_LOB.ISOPEN(l_blob) = 1 THEN DBMS_LOB.CLOSE(l_blob); END IF;
RAISE;
END export_blob_to_file;
/
-- Read from an OS file using BFILE
DECLARE
l_bfile BFILE;
l_buffer RAW(32767);
l_amount BINARY_INTEGER := 32767;
l_offset INTEGER := 1;
BEGIN
-- Point to file on OS (requires CREATE DIRECTORY and GRANT READ)
l_bfile := BFILENAME('IMPORT_DIR', 'input_data.bin');
DBMS_LOB.OPEN(l_bfile, DBMS_LOB.LOB_READONLY);
DBMS_OUTPUT.PUT_LINE('File size: ' || DBMS_LOB.GETLENGTH(l_bfile) || ' bytes');
DBMS_LOB.READ(l_bfile, l_amount, l_offset, l_buffer);
DBMS_OUTPUT.PUT_LINE('First bytes (hex): ' || RAWTOHEX(UTL_RAW.SUBSTR(l_buffer, 1, 16)));
DBMS_LOB.CLOSE(l_bfile);
END;
/
-- Concatenate CLOBs from multiple rows into one
DECLARE
l_result CLOB;
l_sep CONSTANT VARCHAR2(10) := CHR(10) || '---' || CHR(10);
BEGIN
DBMS_LOB.CREATETEMPORARY(l_result, TRUE);
FOR r IN (SELECT doc_id, doc_name, doc_text
FROM documents
WHERE doc_id BETWEEN 1 AND 10
ORDER BY doc_id) LOOP
IF DBMS_LOB.GETLENGTH(l_result) > 0 THEN
DBMS_LOB.WRITEAPPEND(l_result, LENGTH(l_sep), l_sep);
END IF;
DBMS_LOB.WRITEAPPEND(l_result, LENGTH('=== ' || r.doc_name || ' ==='),
'=== ' || r.doc_name || ' ===');
IF DBMS_LOB.GETLENGTH(r.doc_text) > 0 THEN
DBMS_LOB.APPEND(l_result, r.doc_text);
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE('Merged size: ' || DBMS_LOB.GETLENGTH(l_result));
DBMS_LOB.FREETEMPORARY(l_result);
END;
/
-- Replace a substring in a CLOB (no direct REPLACE for LOBs)
CREATE OR REPLACE FUNCTION clob_replace (
p_clob IN CLOB,
p_search IN VARCHAR2,
p_replace IN VARCHAR2
) RETURN CLOB
IS
l_result CLOB;
l_pos INTEGER;
l_offset INTEGER := 1;
l_clob_len INTEGER;
l_search_len INTEGER := LENGTH(p_search);
BEGIN
DBMS_LOB.CREATETEMPORARY(l_result, TRUE);
l_clob_len := DBMS_LOB.GETLENGTH(p_clob);
LOOP
l_pos := DBMS_LOB.INSTR(p_clob, p_search, l_offset);
EXIT WHEN l_pos = 0 OR l_pos IS NULL;
-- Copy text before the match
IF l_pos > l_offset THEN
DECLARE l_piece CLOB; BEGIN
DBMS_LOB.CREATETEMPORARY(l_piece, TRUE);
DBMS_LOB.COPY(l_piece, p_clob, l_pos - l_offset, 1, l_offset);
DBMS_LOB.APPEND(l_result, l_piece);
DBMS_LOB.FREETEMPORARY(l_piece);
END;
END IF;
-- Insert replacement
IF LENGTH(p_replace) > 0 THEN
DBMS_LOB.WRITEAPPEND(l_result, LENGTH(p_replace), p_replace);
END IF;
l_offset := l_pos + l_search_len;
END LOOP;
-- Copy remaining text after last match
IF l_offset <= l_clob_len THEN
DECLARE l_tail CLOB; BEGIN
DBMS_LOB.CREATETEMPORARY(l_tail, TRUE);
DBMS_LOB.COPY(l_tail, p_clob, l_clob_len - l_offset + 1, 1, l_offset);
DBMS_LOB.APPEND(l_result, l_tail);
DBMS_LOB.FREETEMPORARY(l_tail);
END;
END IF;
RETURN l_result;
END clob_replace;
/
  • Use OPEN/CLOSE around batch operations. Opening a LOB before multiple reads or writes suppresses index and trigger overhead per operation.
  • Choose the right chunk size. Match the chunk size to the LOB chunk size (usually 8192 bytes) for best I/O efficiency. Query DBA_LOBS.CHUNK to confirm.
  • Prefer WRITEAPPEND over WRITE when appending sequentially — it avoids offset arithmetic and is faster.
  • Use temporary LOBs for intermediate processing rather than inserting incomplete rows into permanent tables.
  • Set CACHE for frequently read LOBs and NOCACHE NOLOGGING for staging or bulk-loaded LOBs.
  • Always call FREETEMPORARY for temporary LOBs, even in exception handlers, or the space leaks for the session lifetime.
  • Lock the row (SELECT … FOR UPDATE) before writing to a persistent LOB locator to avoid ORA-22920 (row not locked).
  • Check ISOPEN before CLOSE in exception handlers to avoid ORA-22289.
  • Prefer SecureFile storage for all new LOB columns — it supports deduplication, compression, and encryption.
-- Monitor LOB segment sizes
SELECT
owner,
table_name,
column_name,
securefile,
segment_name,
ROUND(s.bytes / 1024 / 1024, 2) AS size_mb,
s.extents
FROM dba_lobs l
JOIN dba_segments s ON s.segment_name = l.segment_name AND s.owner = l.owner
WHERE l.owner NOT IN ('SYS', 'SYSTEM', 'MDSYS')
ORDER BY s.bytes DESC
FETCH FIRST 20 ROWS ONLY;
-- Find tables with large LOB content
SELECT
owner,
table_name,
column_name,
COUNT(*) AS row_count,
ROUND(SUM(DBMS_LOB.GETLENGTH(doc_text)) / 1024 / 1024, 2) AS total_mb
FROM documents
CROSS JOIN dba_lobs
WHERE dba_lobs.table_name = 'DOCUMENTS'
GROUP BY owner, table_name, column_name;
OperationPrivilege Required
Read LOBs in your own schemaNone (object ownership)
Read LOBs in other schemasSELECT on the table
Write LOBsUPDATE on the table
Create temporary LOBsCREATE SESSION, temporary tablespace quota
Use DBMS_LOBGranted by default via PUBLIC