Skip to content

Oracle PL/SQL Best Practices

This guide covers essential Oracle PL/SQL programming best practices for writing efficient, maintainable, and secure database code.

🎯 Core Oracle Best Practices

Naming Conventions

  • Variables: v_variable_name (e.g., v_student_name, v_total_amount)
  • Constants: c_constant_name (e.g., c_max_retry_attempts, c_default_timeout)
  • Cursors: cursor_name (e.g., student_cursor, order_cursor)
  • Procedures/Functions: procedure_name, function_name (e.g., calculate_total, get_student_info)
  • Packages: package_name (e.g., student_utils, order_management)
  • Tables/Columns: Use meaningful names (e.g., students, student_id, first_name)

Code Organization

  • Package Structure: Group related procedures and functions
  • Exception Handling: Comprehensive error handling with custom exceptions
  • Documentation: Comment complex logic and business rules
  • Modular Design: Small, focused procedures and functions

Performance Optimization

  • Bulk Operations: Use BULK COLLECT and FORALL for large datasets
  • Index Usage: Write queries to use existing indexes
  • Bind Variables: Use bind variables instead of string concatenation
  • Minimal Context Switches: Reduce PL/SQL to SQL context switches

🔧 Oracle-Specific Techniques

Exception Handling

-- Good: Comprehensive exception handling
CREATE OR REPLACE PROCEDURE update_student_grade(
    p_student_id IN students.student_id%TYPE,
    p_grade IN students.grade%TYPE
) AS
    v_student_count NUMBER;
    e_invalid_student EXCEPTION;
BEGIN
    -- Validate student exists
    SELECT COUNT(*) INTO v_student_count
    FROM students
    WHERE student_id = p_student_id;

    IF v_student_count = 0 THEN
        RAISE e_invalid_student;
    END IF;

    -- Update grade
    UPDATE students
    SET grade = p_grade,
        last_updated = SYSDATE
    WHERE student_id = p_student_id;

    COMMIT;

EXCEPTION
    WHEN e_invalid_student THEN
        DBMS_OUTPUT.PUT_LINE('Error: Student ID ' || p_student_id || ' not found');
        ROLLBACK;
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Unexpected error: ' || SQLERRM);
        ROLLBACK;
END;
/

-- Bad: No exception handling
CREATE OR REPLACE PROCEDURE update_student_grade_bad(
    p_student_id IN NUMBER,
    p_grade IN VARCHAR2
) AS
BEGIN
    UPDATE students
    SET grade = p_grade
    WHERE student_id = p_student_id;
    COMMIT;
END;
/

Cursor Usage

-- Good: Explicit cursor with proper exception handling
CREATE OR REPLACE PROCEDURE process_student_report AS
    CURSOR student_cursor IS
        SELECT student_id, first_name, last_name, grade
        FROM students
        WHERE grade IS NOT NULL
        ORDER BY last_name, first_name;

    v_student student_cursor%ROWTYPE;
BEGIN
    OPEN student_cursor;

    LOOP
        FETCH student_cursor INTO v_student;
        EXIT WHEN student_cursor%NOTFOUND;

        -- Process student record
        DBMS_OUTPUT.PUT_LINE(
            'Student: ' || v_student.first_name || ' ' || 
            v_student.last_name || ', Grade: ' || v_student.grade
        );
    END LOOP;

    CLOSE student_cursor;

EXCEPTION
    WHEN OTHERS THEN
        IF student_cursor%ISOPEN THEN
            CLOSE student_cursor;
        END IF;
        DBMS_OUTPUT.PUT_LINE('Error processing students: ' || SQLERRM);
END;
/

-- Better: Implicit cursor (simpler and more efficient)
CREATE OR REPLACE PROCEDURE process_student_report_implicit AS
BEGIN
    FOR v_student IN (
        SELECT student_id, first_name, last_name, grade
        FROM students
        WHERE grade IS NOT NULL
        ORDER BY last_name, first_name
    ) LOOP
        DBMS_OUTPUT.PUT_LINE(
            'Student: ' || v_student.first_name || ' ' || 
            v_student.last_name || ', Grade: ' || v_student.grade
        );
    END LOOP;
END;
/

Bulk Operations

-- Good: Bulk operations for performance
CREATE OR REPLACE PROCEDURE bulk_update_grades AS
    TYPE grade_table_type IS TABLE OF students.student_id%TYPE INDEX BY PLS_INTEGER;
    TYPE new_grade_table_type IS TABLE OF students.grade%TYPE INDEX BY PLS_INTEGER;

    v_student_ids grade_table_type;
    v_new_grades new_grade_table_type;
    v_count PLS_INTEGER := 0;
BEGIN
    -- Populate collections (in real scenario, this would come from parameters)
    v_student_ids(1) := 1001;
    v_new_grades(1) := 'A';
    v_student_ids(2) := 1002;
    v_new_grades(2) := 'B';
    v_student_ids(3) := 1003;
    v_new_grades(3) := 'A';
    v_count := 3;

    -- Bulk update using FORALL
    FORALL i IN 1..v_count
        UPDATE students
        SET grade = v_new_grades(i),
            last_updated = SYSDATE
        WHERE student_id = v_student_ids(i);

    COMMIT;

    DBMS_OUTPUT.PUT_LINE('Updated ' || SQL%ROWCOUNT || ' student grades');
END;
/

-- Bad: Row-by-row processing
CREATE OR REPLACE PROCEDURE update_grades_slow AS
BEGIN
    UPDATE students SET grade = 'A' WHERE student_id = 1001;
    UPDATE students SET grade = 'B' WHERE student_id = 1002;
    UPDATE students SET grade = 'A' WHERE student_id = 1003;
    COMMIT;
END;
/

Dynamic SQL with Bind Variables

-- Good: Safe dynamic SQL with bind variables
CREATE OR REPLACE FUNCTION get_student_count(
    p_table_name IN VARCHAR2,
    p_grade_filter IN VARCHAR2 DEFAULT NULL
) RETURN NUMBER AS
    v_sql VARCHAR2(1000);
    v_count NUMBER;
BEGIN
    v_sql := 'SELECT COUNT(*) FROM ' || p_table_name;

    IF p_grade_filter IS NOT NULL THEN
        v_sql := v_sql || ' WHERE grade = :grade_filter';
    END IF;

    EXECUTE IMMEDIATE v_sql INTO v_count 
        USING p_grade_filter;

    RETURN v_count;
END;
/

-- Bad: Unsafe dynamic SQL (SQL injection risk)
CREATE OR REPLACE FUNCTION get_student_count_unsafe(
    p_table_name IN VARCHAR2,
    p_grade_filter IN VARCHAR2 DEFAULT NULL
) RETURN NUMBER AS
    v_sql VARCHAR2(1000);
    v_count NUMBER;
BEGIN
    v_sql := 'SELECT COUNT(*) FROM ' || p_table_name;

    IF p_grade_filter IS NOT NULL THEN
        v_sql := v_sql || ' WHERE grade = ''' || p_grade_filter || '''';
    END IF;

    EXECUTE IMMEDIATE v_sql INTO v_count;

    RETURN v_count;
END;
/

⚠️ Common Oracle Pitfalls

SQL Injection

Using string concatenation in dynamic SQL.

Problem

-- Bad: Vulnerable to SQL injection
CREATE OR REPLACE PROCEDURE get_student_by_name(
    p_name IN VARCHAR2
) AS
    v_sql VARCHAR2(1000);
    v_student students%ROWTYPE;
BEGIN
    v_sql := 'SELECT * FROM students WHERE name = ''' || p_name || '''';
    EXECUTE IMMEDIATE v_sql INTO v_student;
END;
/

Solutions

-- Solution 1: Use bind variables
CREATE OR REPLACE PROCEDURE get_student_by_name_safe(
    p_name IN VARCHAR2
) AS
    v_sql VARCHAR2(1000);
    v_student students%ROWTYPE;
BEGIN
    v_sql := 'SELECT * FROM students WHERE name = :student_name';
    EXECUTE IMMEDIATE v_sql INTO v_student USING p_name;
END;
/

-- Solution 2: Use static SQL when possible
CREATE OR REPLACE PROCEDURE get_student_by_name_static(
    p_name IN students.name%TYPE
) AS
    v_student students%ROWTYPE;
BEGIN
    SELECT * INTO v_student
    FROM students
    WHERE name = p_name;
END;
/

Cursor Memory Leaks

Not closing cursors properly.

Problem

-- Bad: Cursor not closed
CREATE OR REPLACE PROCEDURE process_students_bad AS
    CURSOR student_cursor IS
        SELECT * FROM students;

    v_student student_cursor%ROWTYPE;
BEGIN
    OPEN student_cursor;
    FETCH student_cursor INTO v_student;
    -- Forgot to close cursor!
END;
/

Solutions

-- Solution 1: Explicit close with exception handling
CREATE OR REPLACE PROCEDURE process_students_good AS
    CURSOR student_cursor IS
        SELECT * FROM students;

    v_student student_cursor%ROWTYPE;
BEGIN
    OPEN student_cursor;
    FETCH student_cursor INTO v_student;
    CLOSE student_cursor;

EXCEPTION
    WHEN OTHERS THEN
        IF student_cursor%ISOPEN THEN
            CLOSE student_cursor;
        END IF;
        RAISE;
END;
/

-- Solution 2: Use implicit cursor (no close needed)
CREATE OR REPLACE PROCEDURE process_students_implicit AS
BEGIN
    FOR v_student IN (SELECT * FROM students) LOOP
        -- Process student
        NULL;
    END LOOP;
END;
/

Transaction Management

Improper commit/rollback handling.

Problem

-- Bad: Commit in loop (performance issues)
CREATE OR REPLACE PROCEDURE process_orders_bad AS
BEGIN
    FOR v_order IN (SELECT * FROM orders) LOOP
        UPDATE order_items
        SET processed = 1
        WHERE order_id = v_order.order_id;
        COMMIT;  -- Bad: Commit in loop!
    END LOOP;
END;
/

Solutions

-- Solution 1: Single transaction
CREATE OR REPLACE PROCEDURE process_orders_good AS
BEGIN
    FOR v_order IN (SELECT * FROM orders) LOOP
        UPDATE order_items
        SET processed = 1
        WHERE order_id = v_order.order_id;
    END LOOP;
    COMMIT;  -- Single commit after all updates
END;
/

-- Solution 2: Batch processing for large datasets
CREATE OR REPLACE PROCEDURE process_orders_batch AS
    v_batch_size CONSTANT NUMBER := 1000;
    v_processed NUMBER := 0;
BEGIN
    FOR v_order IN (SELECT * FROM orders) LOOP
        UPDATE order_items
        SET processed = 1
        WHERE order_id = v_order.order_id;

        v_processed := v_processed + 1;

        IF MOD(v_processed, v_batch_size) = 0 THEN
            COMMIT;  -- Commit every 1000 records
        END IF;
    END LOOP;

    IF MOD(v_processed, v_batch_size) != 0 THEN
        COMMIT;  -- Final commit
    END IF;
END;
/

🚀 Performance Optimization

Index Usage

-- Good: Queries use indexes effectively
SELECT student_id, first_name, last_name
FROM students
WHERE last_name = 'Smith' AND grade = 'A';  -- Assumes indexes on last_name, grade

-- Bad: Functions on indexed columns prevent index usage
SELECT student_id, first_name, last_name
FROM students
WHERE UPPER(last_name) = 'SMITH';  -- Can't use index on last_name

Bind Variables

-- Good: Uses bind variables (shared cursor)
SELECT * FROM students WHERE grade = :grade;

-- Bad: Hard-coded values (new cursor each time)
SELECT * FROM students WHERE grade = 'A';

Bulk Operations

-- Good: Bulk collect and forall
DECLARE
    TYPE student_table_type IS TABLE OF students%ROWTYPE;
    v_students student_table_type;
BEGIN
    SELECT * BULK COLLECT INTO v_students
    FROM large_dataset;

    FORALL i IN 1..v_students.COUNT
        INSERT INTO student_archive VALUES v_students(i);
END;
/

🛠️ Debugging Best Practices

DBMS_OUTPUT Usage

-- Good: Structured debugging output
CREATE OR REPLACE PROCEDURE debug_process(
    p_step IN VARCHAR2,
    p_message IN VARCHAR2
) AS
BEGIN
    DBMS_OUTPUT.PUT_LINE(
        '[DEBUG ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') || 
        '] ' || p_step || ': ' || p_message
    );
END;
/

-- Usage in procedures
BEGIN
    debug_process('START', 'Beginning student processing');

    -- Processing logic here

    debug_process('END', 'Student processing completed');
END;
/

Error Logging

-- Create error logging table
CREATE TABLE error_log (
    log_id NUMBER GENERATED ALWAYS AS IDENTITY,
    error_time TIMESTAMP DEFAULT SYSDATE,
    procedure_name VARCHAR2(100),
    error_code NUMBER,
    error_message VARCHAR2(4000),
    additional_info VARCHAR2(1000)
);

-- Error logging procedure
CREATE OR REPLACE PROCEDURE log_error(
    p_procedure_name IN VARCHAR2,
    p_error_code IN NUMBER DEFAULT NULL,
    p_error_message IN VARCHAR2 DEFAULT NULL,
    p_additional_info IN VARCHAR2 DEFAULT NULL
) AS
BEGIN
    INSERT INTO error_log (
        procedure_name,
        error_code,
        error_message,
        additional_info
    ) VALUES (
        p_procedure_name,
        p_error_code,
        p_error_message,
        p_additional_info
    );

    COMMIT;
END;
/

🔗 Common Programming Best Practices