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.
Overview
Section titled “Overview”LOB Types
Section titled “LOB Types”| Type | Description | Max Size |
|---|---|---|
CLOB | Character Large Object (database character set) | 128 TB |
NCLOB | National Character Large Object (Unicode) | 128 TB |
BLOB | Binary Large Object (raw bytes) | 128 TB |
BFILE | Binary file stored on OS filesystem (read-only) | OS limit |
SecureFile vs BasicFile LOBs
Section titled “SecureFile vs BasicFile LOBs”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 tablesSELECT t.table_name, l.column_name, l.securefile, l.in_row, l.chunk, l.cache, l.logging, l.compression, l.deduplicationFROM dba_lobs lJOIN dba_tables t ON l.table_name = t.table_name AND l.owner = t.ownerWHERE l.owner = 'SCOTT'ORDER BY t.table_name, l.column_name;Key Functions and Procedures
Section titled “Key Functions and Procedures”GETLENGTH
Section titled “GETLENGTH”Returns the length of a LOB value in bytes (BLOB/BFILE) or characters (CLOB/NCLOB).
-- SyntaxDBMS_LOB.GETLENGTH(lob_loc IN BLOB|CLOB|BFILE) RETURN INTEGER;
-- Example: check document sizesSELECT doc_id, doc_name, DBMS_LOB.GETLENGTH(doc_text) AS clob_chars, DBMS_LOB.GETLENGTH(doc_data) AS blob_bytesFROM documentsWHERE DBMS_LOB.GETLENGTH(doc_text) > 0ORDER BY DBMS_LOB.GETLENGTH(doc_text) DESC;SUBSTR
Section titled “SUBSTR”Extracts a substring from a CLOB. Equivalent to SQL SUBSTR but works on full LOB content beyond 32767 characters.
-- SyntaxDBMS_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 previewSELECT doc_id, doc_name, DBMS_LOB.SUBSTR(doc_text, 200, 1) AS previewFROM documents;
-- Find content starting at a known positionSELECT DBMS_LOB.SUBSTR(doc_text, 500, 10001) AS mid_sectionFROM documentsWHERE doc_id = 42;Reads a portion of LOB data into a buffer. Used in procedural code for chunked processing.
-- SyntaxDBMS_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).
-- SyntaxDBMS_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 contentDECLARE 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;/APPEND
Section titled “APPEND”Appends the content of one LOB to another. Efficient for building LOBs incrementally.
-- SyntaxDBMS_LOB.APPEND( dest_lob IN OUT NOCOPY BLOB|CLOB, src_lob IN BLOB|CLOB);
-- Append one document's text to anotherDECLARE 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 WRITEAPPENDDECLARE 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.
-- SyntaxDBMS_LOB.TRIM( lob_loc IN OUT NOCOPY BLOB|CLOB, newlen IN INTEGER);
-- Trim a CLOB to first 1000 charactersDECLARE 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.
-- SyntaxDBMS_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 1001DECLARE 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.
-- SyntaxDBMS_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;/CREATETEMPORARY and FREETEMPORARY
Section titled “CREATETEMPORARY and FREETEMPORARY”Creates a temporary LOB in the temporary tablespace for in-memory processing without a permanent table row.
-- SyntaxDBMS_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 CLOBDECLARE 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;/OPEN, CLOSE, and ISOPEN
Section titled “OPEN, CLOSE, and ISOPEN”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_READWRITEDECLARE 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.
-- SyntaxDBMS_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 keywordDECLARE 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;/Practical Patterns
Section titled “Practical Patterns”Chunked Read for Large LOBs
Section titled “Chunked Read for Large LOBs”Reading a LOB larger than 32767 characters/bytes requires looping with offsets.
-- Process a CLOB larger than 32767 characters in chunksDECLARE 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;/Chunked Write for Large LOBs
Section titled “Chunked Write for Large LOBs”-- Write a large CLOB from VARCHAR2 chunksCREATE 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;/CLOB to VARCHAR2 Conversion
Section titled “CLOB to VARCHAR2 Conversion”For CLOBs that fit within 32767 characters, a direct cast works. For larger CLOBs, convert in chunks.
-- Safe CLOB to VARCHAR2 for CLOBs <= 32767 charsCREATE OR REPLACE FUNCTION clob_to_varchar2 (p_clob IN CLOB)RETURN VARCHAR2IS 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 querySELECT doc_id, clob_to_varchar2(doc_text) AS text_previewFROM documentsWHERE DBMS_LOB.GETLENGTH(doc_text) <= 32767;BLOB Export to File via UTL_FILE
Section titled “BLOB Export to File via UTL_FILE”-- Export a BLOB to the filesystem in raw chunksCREATE 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;/BFILE Reading
Section titled “BFILE Reading”-- Read from an OS file using BFILEDECLARE 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;/Common Patterns
Section titled “Common Patterns”Aggregate CLOBs Across Rows
Section titled “Aggregate CLOBs Across Rows”-- Concatenate CLOBs from multiple rows into oneDECLARE 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;/Search and Replace in a CLOB
Section titled “Search and Replace in a CLOB”-- 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 CLOBIS 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;/Best Practices
Section titled “Best Practices”Performance
Section titled “Performance”- 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.CHUNKto 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
CACHEfor frequently read LOBs andNOCACHE NOLOGGINGfor staging or bulk-loaded LOBs.
Correctness
Section titled “Correctness”- 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.
Querying LOB Metadata
Section titled “Querying LOB Metadata”-- Monitor LOB segment sizesSELECT owner, table_name, column_name, securefile, segment_name, ROUND(s.bytes / 1024 / 1024, 2) AS size_mb, s.extentsFROM dba_lobs lJOIN dba_segments s ON s.segment_name = l.segment_name AND s.owner = l.ownerWHERE l.owner NOT IN ('SYS', 'SYSTEM', 'MDSYS')ORDER BY s.bytes DESCFETCH FIRST 20 ROWS ONLY;
-- Find tables with large LOB contentSELECT owner, table_name, column_name, COUNT(*) AS row_count, ROUND(SUM(DBMS_LOB.GETLENGTH(doc_text)) / 1024 / 1024, 2) AS total_mbFROM documentsCROSS JOIN dba_lobsWHERE dba_lobs.table_name = 'DOCUMENTS'GROUP BY owner, table_name, column_name;Required Privileges
Section titled “Required Privileges”| Operation | Privilege Required |
|---|---|
| Read LOBs in your own schema | None (object ownership) |
| Read LOBs in other schemas | SELECT on the table |
| Write LOBs | UPDATE on the table |
| Create temporary LOBs | CREATE SESSION, temporary tablespace quota |
Use DBMS_LOB | Granted by default via PUBLIC |
Related Topics
Section titled “Related Topics”- UTL_FILE - Write LOB data to OS files
- DBMS_METADATA - Extract DDL for tables with LOB columns
- Oracle Data Pump - Export/import tables containing LOBs