Skip to content

PostgreSQL Large Object Storage Best Practices

Objective: Master senior-level PostgreSQL large object storage patterns for production systems. When you need to handle BLOBs, when you want to optimize large object storage, when you need enterprise-grade large object strategies—these best practices become your weapon of choice.

Core Principles

  • Storage Efficiency: Choose appropriate storage methods for large objects
  • Performance: Optimize large object access and retrieval
  • Security: Implement proper access controls for large objects
  • Backup: Ensure large objects are included in backup strategies
  • Maintenance: Implement cleanup and maintenance procedures

Large Object Storage Methods

BYTEA vs Large Objects

-- Create tables for different large object storage methods
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    filename VARCHAR(255) NOT NULL,
    content_type VARCHAR(100) NOT NULL,
    file_size BIGINT NOT NULL,
    content BYTEA,                           -- BYTEA for smaller files (< 1GB)
    metadata JSONB,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE large_files (
    id SERIAL PRIMARY KEY,
    filename VARCHAR(255) NOT NULL,
    content_type VARCHAR(100) NOT NULL,
    file_size BIGINT NOT NULL,
    oid OID,                                -- Large Object OID for larger files
    metadata JSONB,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE file_references (
    id SERIAL PRIMARY KEY,
    filename VARCHAR(255) NOT NULL,
    content_type VARCHAR(100) NOT NULL,
    file_size BIGINT NOT NULL,
    storage_path VARCHAR(500) NOT NULL,     -- External storage path
    checksum VARCHAR(64) NOT NULL,          -- File integrity checksum
    metadata JSONB,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

Large Object Functions

-- Create function to store large object
CREATE OR REPLACE FUNCTION store_large_object(
    p_filename VARCHAR(255),
    p_content_type VARCHAR(100),
    p_file_data BYTEA
)
RETURNS INTEGER AS $$
DECLARE
    file_oid OID;
    file_size BIGINT;
BEGIN
    -- Create large object
    file_oid := lo_create(0);

    -- Open large object for writing
    PERFORM lo_open(file_oid, 131072); -- 131072 = INV_WRITE

    -- Write data to large object
    PERFORM lo_write(file_oid, 0, p_file_data);

    -- Close large object
    PERFORM lo_close(file_oid);

    -- Get file size
    file_size := LENGTH(p_file_data);

    -- Insert record
    INSERT INTO large_files (filename, content_type, file_size, oid)
    VALUES (p_filename, p_content_type, file_size, file_oid);

    RETURN file_oid;
END;
$$ LANGUAGE plpgsql;

-- Create function to retrieve large object
CREATE OR REPLACE FUNCTION retrieve_large_object(p_oid OID)
RETURNS BYTEA AS $$
DECLARE
    file_data BYTEA;
    file_size BIGINT;
BEGIN
    -- Get file size
    SELECT file_size INTO file_size
    FROM large_files
    WHERE oid = p_oid;

    -- Open large object for reading
    PERFORM lo_open(p_oid, 262144); -- 262144 = INV_READ

    -- Read data from large object
    SELECT lo_read(p_oid, 0, file_size) INTO file_data;

    -- Close large object
    PERFORM lo_close(p_oid);

    RETURN file_data;
END;
$$ LANGUAGE plpgsql;

File Management System

File Upload and Storage

-- Create function for file upload
CREATE OR REPLACE FUNCTION upload_file(
    p_filename VARCHAR(255),
    p_content_type VARCHAR(100),
    p_file_data BYTEA,
    p_metadata JSONB DEFAULT '{}'
)
RETURNS INTEGER AS $$
DECLARE
    file_id INTEGER;
    file_size BIGINT;
    file_checksum VARCHAR(64);
BEGIN
    -- Calculate file size and checksum
    file_size := LENGTH(p_file_data);
    file_checksum := encode(digest(p_file_data, 'sha256'), 'hex');

    -- Check if file already exists
    IF EXISTS (SELECT 1 FROM file_references WHERE checksum = file_checksum) THEN
        RAISE EXCEPTION 'File already exists with checksum: %', file_checksum;
    END IF;

    -- Store file based on size
    IF file_size < 1048576 THEN -- 1MB
        -- Store small files in BYTEA
        INSERT INTO documents (filename, content_type, file_size, content, metadata)
        VALUES (p_filename, p_content_type, file_size, p_file_data, p_metadata)
        RETURNING id INTO file_id;
    ELSE
        -- Store large files as Large Objects
        INSERT INTO large_files (filename, content_type, file_size, oid, metadata)
        VALUES (p_filename, p_content_type, file_size, 
                store_large_object(p_filename, p_content_type, p_file_data), p_metadata)
        RETURNING id INTO file_id;
    END IF;

    RETURN file_id;
END;
$$ LANGUAGE plpgsql;

-- Create function for file download
CREATE OR REPLACE FUNCTION download_file(p_file_id INTEGER)
RETURNS TABLE (
    filename VARCHAR(255),
    content_type VARCHAR(100),
    file_size BIGINT,
    file_data BYTEA,
    metadata JSONB
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        d.filename,
        d.content_type,
        d.file_size,
        d.content as file_data,
        d.metadata
    FROM documents d
    WHERE d.id = p_file_id

    UNION ALL

    SELECT 
        lf.filename,
        lf.content_type,
        lf.file_size,
        retrieve_large_object(lf.oid) as file_data,
        lf.metadata
    FROM large_files lf
    WHERE lf.id = p_file_id;
END;
$$ LANGUAGE plpgsql;

File Integrity and Validation

-- Create function to validate file integrity
CREATE OR REPLACE FUNCTION validate_file_integrity(p_file_id INTEGER)
RETURNS BOOLEAN AS $$
DECLARE
    stored_checksum VARCHAR(64);
    calculated_checksum VARCHAR(64);
    file_data BYTEA;
BEGIN
    -- Get stored checksum and file data
    SELECT checksum, content INTO stored_checksum, file_data
    FROM documents
    WHERE id = p_file_id;

    IF stored_checksum IS NULL THEN
        -- Check large files table
        SELECT checksum, retrieve_large_object(oid) INTO stored_checksum, file_data
        FROM large_files
        WHERE id = p_file_id;
    END IF;

    IF stored_checksum IS NULL THEN
        RETURN FALSE;
    END IF;

    -- Calculate current checksum
    calculated_checksum := encode(digest(file_data, 'sha256'), 'hex');

    -- Compare checksums
    RETURN stored_checksum = calculated_checksum;
END;
$$ LANGUAGE plpgsql;

-- Create function to update file checksums
CREATE OR REPLACE FUNCTION update_file_checksums()
RETURNS INTEGER AS $$
DECLARE
    file_record RECORD;
    calculated_checksum VARCHAR(64);
    updated_count INTEGER := 0;
BEGIN
    -- Update checksums for documents
    FOR file_record IN 
        SELECT id, content FROM documents WHERE checksum IS NULL
    LOOP
        calculated_checksum := encode(digest(file_record.content, 'sha256'), 'hex');

        UPDATE documents 
        SET checksum = calculated_checksum
        WHERE id = file_record.id;

        updated_count := updated_count + 1;
    END LOOP;

    -- Update checksums for large files
    FOR file_record IN 
        SELECT id, oid FROM large_files WHERE checksum IS NULL
    LOOP
        calculated_checksum := encode(digest(retrieve_large_object(file_record.oid), 'sha256'), 'hex');

        UPDATE large_files 
        SET checksum = calculated_checksum
        WHERE id = file_record.id;

        updated_count := updated_count + 1;
    END LOOP;

    RETURN updated_count;
END;
$$ LANGUAGE plpgsql;

External Storage Integration

S3-Compatible Storage

-- Create function for S3 storage integration
CREATE OR REPLACE FUNCTION store_file_s3(
    p_filename VARCHAR(255),
    p_content_type VARCHAR(100),
    p_file_data BYTEA,
    p_bucket_name VARCHAR(100),
    p_metadata JSONB DEFAULT '{}'
)
RETURNS INTEGER AS $$
DECLARE
    file_id INTEGER;
    file_size BIGINT;
    file_checksum VARCHAR(64);
    storage_path VARCHAR(500);
BEGIN
    -- Calculate file size and checksum
    file_size := LENGTH(p_file_data);
    file_checksum := encode(digest(p_file_data, 'sha256'), 'hex');

    -- Generate storage path
    storage_path := p_bucket_name || '/' || 
                   to_char(CURRENT_DATE, 'YYYY/MM/DD') || '/' ||
                   file_checksum || '_' || p_filename;

    -- Store file reference
    INSERT INTO file_references (filename, content_type, file_size, storage_path, checksum, metadata)
    VALUES (p_filename, p_content_type, file_size, storage_path, file_checksum, p_metadata)
    RETURNING id INTO file_id;

    -- Note: Actual S3 upload would be handled by application code
    -- This function only creates the database record

    RETURN file_id;
END;
$$ LANGUAGE plpgsql;

-- Create function to retrieve file from S3
CREATE OR REPLACE FUNCTION retrieve_file_s3(p_file_id INTEGER)
RETURNS TABLE (
    filename VARCHAR(255),
    content_type VARCHAR(100),
    file_size BIGINT,
    storage_path VARCHAR(500),
    checksum VARCHAR(64),
    metadata JSONB
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        fr.filename,
        fr.content_type,
        fr.file_size,
        fr.storage_path,
        fr.checksum,
        fr.metadata
    FROM file_references fr
    WHERE fr.id = p_file_id;
END;
$$ LANGUAGE plpgsql;

File Cleanup and Maintenance

-- Create function for file cleanup
CREATE OR REPLACE FUNCTION cleanup_orphaned_files()
RETURNS INTEGER AS $$
DECLARE
    orphaned_count INTEGER := 0;
    file_record RECORD;
BEGIN
    -- Find orphaned large objects
    FOR file_record IN 
        SELECT oid FROM large_files 
        WHERE NOT EXISTS (
            SELECT 1 FROM pg_largeobject_metadata 
            WHERE oid = large_files.oid
        )
    LOOP
        -- Delete orphaned large object
        PERFORM lo_unlink(file_record.oid);
        orphaned_count := orphaned_count + 1;
    END LOOP;

    -- Delete orphaned file references
    DELETE FROM file_references 
    WHERE storage_path IS NOT NULL 
    AND NOT EXISTS (
        SELECT 1 FROM file_references fr2 
        WHERE fr2.storage_path = file_references.storage_path
    );

    GET DIAGNOSTICS orphaned_count = orphaned_count + ROW_COUNT;

    RETURN orphaned_count;
END;
$$ LANGUAGE plpgsql;

-- Create function for file size analysis
CREATE OR REPLACE FUNCTION analyze_file_storage()
RETURNS TABLE (
    storage_type TEXT,
    file_count BIGINT,
    total_size BIGINT,
    avg_size NUMERIC,
    max_size BIGINT,
    min_size BIGINT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        'BYTEA'::TEXT as storage_type,
        COUNT(*) as file_count,
        SUM(file_size) as total_size,
        AVG(file_size) as avg_size,
        MAX(file_size) as max_size,
        MIN(file_size) as min_size
    FROM documents

    UNION ALL

    SELECT 
        'Large Objects'::TEXT as storage_type,
        COUNT(*) as file_count,
        SUM(file_size) as total_size,
        AVG(file_size) as avg_size,
        MAX(file_size) as max_size,
        MIN(file_size) as min_size
    FROM large_files

    UNION ALL

    SELECT 
        'External Storage'::TEXT as storage_type,
        COUNT(*) as file_count,
        SUM(file_size) as total_size,
        AVG(file_size) as avg_size,
        MAX(file_size) as max_size,
        MIN(file_size) as min_size
    FROM file_references;
END;
$$ LANGUAGE plpgsql;

Large Object Security

Access Control

-- Create function for file access control
CREATE OR REPLACE FUNCTION check_file_access(
    p_file_id INTEGER,
    p_user_id INTEGER,
    p_access_type VARCHAR(20) DEFAULT 'read'
)
RETURNS BOOLEAN AS $$
DECLARE
    file_owner INTEGER;
    file_permissions JSONB;
BEGIN
    -- Get file owner and permissions
    SELECT owner_id, permissions INTO file_owner, file_permissions
    FROM (
        SELECT id, owner_id, permissions FROM documents WHERE id = p_file_id
        UNION ALL
        SELECT id, owner_id, permissions FROM large_files WHERE id = p_file_id
        UNION ALL
        SELECT id, owner_id, permissions FROM file_references WHERE id = p_file_id
    ) t;

    -- Check if user is owner
    IF file_owner = p_user_id THEN
        RETURN TRUE;
    END IF;

    -- Check permissions
    IF file_permissions IS NOT NULL THEN
        IF p_access_type = 'read' AND (file_permissions ? 'read' OR file_permissions ? 'write') THEN
            RETURN TRUE;
        END IF;

        IF p_access_type = 'write' AND file_permissions ? 'write' THEN
            RETURN TRUE;
        END IF;
    END IF;

    RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

-- Create function for secure file retrieval
CREATE OR REPLACE FUNCTION secure_download_file(
    p_file_id INTEGER,
    p_user_id INTEGER
)
RETURNS TABLE (
    filename VARCHAR(255),
    content_type VARCHAR(100),
    file_size BIGINT,
    file_data BYTEA,
    metadata JSONB
) AS $$
BEGIN
    -- Check access permissions
    IF NOT check_file_access(p_file_id, p_user_id, 'read') THEN
        RAISE EXCEPTION 'Access denied for file %', p_file_id;
    END IF;

    -- Return file data
    RETURN QUERY
    SELECT * FROM download_file(p_file_id);
END;
$$ LANGUAGE plpgsql;

File Encryption

-- Create function for file encryption
CREATE OR REPLACE FUNCTION encrypt_file_data(
    p_file_data BYTEA,
    p_encryption_key TEXT
)
RETURNS BYTEA AS $$
BEGIN
    -- Use pgcrypto for encryption
    RETURN pgp_sym_encrypt(p_file_data, p_encryption_key);
END;
$$ LANGUAGE plpgsql;

-- Create function for file decryption
CREATE OR REPLACE FUNCTION decrypt_file_data(
    p_encrypted_data BYTEA,
    p_encryption_key TEXT
)
RETURNS BYTEA AS $$
BEGIN
    -- Use pgcrypto for decryption
    RETURN pgp_sym_decrypt(p_encrypted_data, p_encryption_key);
END;
$$ LANGUAGE plpgsql;

-- Create function for secure file storage
CREATE OR REPLACE FUNCTION store_encrypted_file(
    p_filename VARCHAR(255),
    p_content_type VARCHAR(100),
    p_file_data BYTEA,
    p_encryption_key TEXT,
    p_metadata JSONB DEFAULT '{}'
)
RETURNS INTEGER AS $$
DECLARE
    file_id INTEGER;
    encrypted_data BYTEA;
    file_size BIGINT;
    file_checksum VARCHAR(64);
BEGIN
    -- Encrypt file data
    encrypted_data := encrypt_file_data(p_file_data, p_encryption_key);

    -- Calculate file size and checksum
    file_size := LENGTH(p_file_data);
    file_checksum := encode(digest(p_file_data, 'sha256'), 'hex');

    -- Store encrypted file
    INSERT INTO documents (filename, content_type, file_size, content, metadata)
    VALUES (p_filename, p_content_type, file_size, encrypted_data, p_metadata)
    RETURNING id INTO file_id;

    RETURN file_id;
END;
$$ LANGUAGE plpgsql;

Large Object Monitoring

Storage Monitoring

# monitoring/large_object_monitor.py
import psycopg2
import json
from datetime import datetime
import logging

class LargeObjectMonitor:
    def __init__(self, connection_params):
        self.conn_params = connection_params
        self.setup_logging()

    def setup_logging(self):
        """Setup logging."""
        logging.basicConfig(level=logging.INFO)
        self.logger = logging.getLogger(__name__)

    def get_storage_statistics(self):
        """Get storage statistics."""
        conn = psycopg2.connect(**self.conn_params)

        try:
            with conn.cursor() as cur:
                cur.execute("""
                    SELECT 
                        'documents' as table_name,
                        COUNT(*) as file_count,
                        SUM(file_size) as total_size,
                        AVG(file_size) as avg_size,
                        MAX(file_size) as max_size,
                        MIN(file_size) as min_size
                    FROM documents
                    UNION ALL
                    SELECT 
                        'large_files' as table_name,
                        COUNT(*) as file_count,
                        SUM(file_size) as total_size,
                        AVG(file_size) as avg_size,
                        MAX(file_size) as max_size,
                        MIN(file_size) as min_size
                    FROM large_files
                    UNION ALL
                    SELECT 
                        'file_references' as table_name,
                        COUNT(*) as file_count,
                        SUM(file_size) as total_size,
                        AVG(file_size) as avg_size,
                        MAX(file_size) as max_size,
                        MIN(file_size) as min_size
                    FROM file_references
                """)

                storage_stats = cur.fetchall()
                return storage_stats

        except Exception as e:
            self.logger.error(f"Error getting storage statistics: {e}")
            return []
        finally:
            conn.close()

    def get_large_object_usage(self):
        """Get large object usage statistics."""
        conn = psycopg2.connect(**self.conn_params)

        try:
            with conn.cursor() as cur:
                cur.execute("""
                    SELECT 
                        schemaname,
                        tablename,
                        indexname,
                        idx_scan,
                        idx_tup_read,
                        idx_tup_fetch,
                        pg_size_pretty(pg_relation_size(indexrelid)) as index_size
                    FROM pg_stat_user_indexes
                    WHERE tablename IN ('documents', 'large_files', 'file_references')
                    ORDER BY idx_scan DESC
                """)

                usage_stats = cur.fetchall()
                return usage_stats

        except Exception as e:
            self.logger.error(f"Error getting large object usage: {e}")
            return []
        finally:
            conn.close()

    def get_file_integrity_status(self):
        """Get file integrity status."""
        conn = psycopg2.connect(**self.conn_params)

        try:
            with conn.cursor() as cur:
                cur.execute("""
                    SELECT 
                        'documents' as table_name,
                        COUNT(*) as total_files,
                        COUNT(*) FILTER (WHERE checksum IS NOT NULL) as files_with_checksum,
                        COUNT(*) FILTER (WHERE checksum IS NULL) as files_without_checksum
                    FROM documents
                    UNION ALL
                    SELECT 
                        'large_files' as table_name,
                        COUNT(*) as total_files,
                        COUNT(*) FILTER (WHERE checksum IS NOT NULL) as files_with_checksum,
                        COUNT(*) FILTER (WHERE checksum IS NULL) as files_without_checksum
                    FROM large_files
                """)

                integrity_status = cur.fetchall()
                return integrity_status

        except Exception as e:
            self.logger.error(f"Error getting file integrity status: {e}")
            return []
        finally:
            conn.close()

    def generate_large_object_report(self):
        """Generate comprehensive large object report."""
        storage_stats = self.get_storage_statistics()
        usage_stats = self.get_large_object_usage()
        integrity_status = self.get_file_integrity_status()

        report = {
            'report_timestamp': datetime.now().isoformat(),
            'storage_statistics': storage_stats,
            'usage_statistics': usage_stats,
            'integrity_status': integrity_status
        }

        return report

# Usage
if __name__ == "__main__":
    monitor = LargeObjectMonitor({
        'host': 'localhost',
        'database': 'production',
        'user': 'monitor_user',
        'password': 'monitor_password'
    })

    report = monitor.generate_large_object_report()
    print(json.dumps(report, indent=2))

TL;DR Runbook

Quick Start

-- 1. Create large object tables
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    filename VARCHAR(255) NOT NULL,
    content_type VARCHAR(100) NOT NULL,
    file_size BIGINT NOT NULL,
    content BYTEA,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

-- 2. Create large object functions
CREATE OR REPLACE FUNCTION store_large_object(
    p_filename VARCHAR(255),
    p_content_type VARCHAR(100),
    p_file_data BYTEA
) RETURNS INTEGER AS $$
-- Function implementation here
$$ LANGUAGE plpgsql;

-- 3. Store files
SELECT store_large_object('document.pdf', 'application/pdf', file_data);

-- 4. Retrieve files
SELECT * FROM download_file(1);

Essential Patterns

# Complete PostgreSQL large object storage setup
def setup_postgresql_large_objects():
    # 1. Large object storage methods
    # 2. File management system
    # 3. File integrity and validation
    # 4. External storage integration
    # 5. File cleanup and maintenance
    # 6. Large object security
    # 7. File encryption
    # 8. Storage monitoring

    print("PostgreSQL large object storage setup complete!")

This guide provides the complete machinery for PostgreSQL large object storage excellence. Each pattern includes implementation examples, storage strategies, and real-world usage patterns for enterprise PostgreSQL large object systems.