Data Types


Oracle PL/SQL supports a rich set of data types, defining the kind of values a variable or constant can hold and the operations that can be performed on them. Choosing the correct data type is essential for efficient memory usage, data integrity, and avoiding runtime errors in your PL/SQL programs.

 

Scalar Data Types (NUMBER, VARCHAR2, DATE, BOOLEAN, etc.)

Scalar data types hold a single, indivisible value. They are the most commonly used data types in Oracle PL/SQL.

 

NUMBER

The NUMBER data type is used to store numeric values. It can store integers, fixed-point numbers, and floating-point numbers. It's highly versatile and widely used in Oracle databases.

 

Example 1: Declaring a Simple Integer

DECLARE
    -- Declaring an integer variable
    -- NUMBER without precision/scale implies maximum precision.
    order_quantity NUMBER := 150;
BEGIN
    DBMS_OUTPUT.PUT_LINE('Order Quantity: ' || order_quantity);
END;
/

Explanation This example demonstrates a basic NUMBER declaration for an integer. When no precision or scale is specified, NUMBER can store numbers with maximum precision, making it flexible for various numeric values.

 

Example 2: Declaring a Fixed-Point Number

DECLARE
    -- Declaring a fixed-point number with precision and scale
    -- total 5 digits, 2 digits after the decimal point
    product_price NUMBER(5, 2) := 99.99;
BEGIN
    DBMS_OUTPUT.PUT_LINE('Product Price: ' || product_price);

    -- What happens if we assign a value with more decimal places?
    -- product_price := 123.456; -- Oracle will round this to 123.46
    -- DBMS_OUTPUT.PUT_LINE('Rounded Product Price: ' || product_price);
END;
/

Explanation This example shows NUMBER with specified precision and scale (NUMBER(5, 2)). This is ideal for financial or measurement data where a fixed number of decimal places is required. Oracle will round values if they exceed the specified scale.

 

Example 3: Declaring a Floating-Point Number (BINARY_FLOAT, BINARY_DOUBLE)

DECLARE
    -- Declaring a binary float for single-precision floating-point numbers
    temperature BINARY_FLOAT := 25.75;
    -- Declaring a binary double for double-precision floating-point numbers
    pi_val BINARY_DOUBLE := 3.141592653589793;
BEGIN
    DBMS_OUTPUT.PUT_LINE('Temperature: ' || temperature);
    DBMS_OUTPUT.PUT_LINE('PI Value: ' || pi_val);
END;
/

Explanation This example introduces BINARY_FLOAT and BINARY_DOUBLE for efficient storage and computation of floating-point numbers, particularly useful for scientific and engineering applications where precision is critical.

 

Example 4: Using NUMBER in Calculations and Rounding (Advanced)

DECLARE
    -- Declaring variables for calculations
    net_amount NUMBER(10, 2) := 1250.75;
    tax_rate   NUMBER(3, 2) := 0.08; -- 8%
    gross_amount NUMBER(10, 2);
    rounded_amount NUMBER(10, 2);
BEGIN
    -- Calculating gross amount
    gross_amount := net_amount * (1 + tax_rate);
    DBMS_OUTPUT.PUT_LINE('Net Amount: ' || net_amount);
    DBMS_OUTPUT.PUT_LINE('Gross Amount (unrounded): ' || gross_amount);

    -- Rounding the gross amount to two decimal places
    rounded_amount := ROUND(gross_amount, 2);
    DBMS_OUTPUT.PUT_LINE('Gross Amount (rounded): ' || rounded_amount);

    -- More advanced number functions
    DBMS_OUTPUT.PUT_LINE('CEIL(gross_amount): ' || CEIL(gross_amount)); -- Ceiling
    DBMS_OUTPUT.PUT_LINE('FLOOR(gross_amount): ' || FLOOR(gross_amount)); -- Floor
END;
/

Explanation This example demonstrates NUMBER in practical calculations and introduces built-in SQL functions like ROUND, CEIL, and FLOOR for numeric manipulation, essential for accurate financial and statistical reporting in PL/SQL.

 

Example 5: NUMBER with Scale for Currency Conversion (Advanced)

DECLARE
    -- Exchange rate for USD to EUR, allowing for high precision
    usd_to_eur_rate NUMBER(10, 8) := 0.93256789;
    -- Amount in USD
    amount_usd      NUMBER(12, 2) := 1000.00;
    -- Converted amount in EUR
    amount_eur      NUMBER(12, 2);
BEGIN
    -- Performing currency conversion
    amount_eur := amount_usd * usd_to_eur_rate;

    DBMS_OUTPUT.PUT_LINE('Amount in USD: ' || amount_usd);
    DBMS_OUTPUT.PUT_LINE('USD to EUR Rate: ' || usd_to_eur_rate);
    DBMS_OUTPUT.PUT_LINE('Amount in EUR: ' || ROUND(amount_eur, 2)); -- Rounding to typical currency precision

    -- Demonstrate storing large numbers
    DECLARE
        large_population NUMBER(15) := 8100000000;
    BEGIN
        DBMS_OUTPUT.PUT_LINE('World Population (approx): ' || large_population);
    END;
END;
/

Explanation This advanced example highlights the flexibility of the NUMBER data type for handling currency exchange rates with high precision and for storing very large integer values. It reinforces the importance of choosing appropriate precision and scale for specific data requirements in Oracle PL/SQL.

 

VARCHAR2

The VARCHAR2 data type is used to store variable-length character strings. It's the most common data type for textual information in Oracle PL/SQL.

 

Example 1: Declaring a Simple VARCHAR2

DECLARE
    -- Declaring a VARCHAR2 variable for a city name
    city_name VARCHAR2(100) := 'Seattle';
BEGIN
    DBMS_OUTPUT.PUT_LINE('City: ' || city_name);
END;
/

Explanation This basic example demonstrates the declaration of a VARCHAR2 variable with a specified maximum length of 100 characters, ideal for storing variable-length textual data like names or addresses.

 

Example 2: VARCHAR2 with Character Functions

DECLARE
    -- Declaring a VARCHAR2 variable for a customer name
    customer_name VARCHAR2(50) := 'Alice Wonderland';
    -- Variable to store the uppercase version of the name
    upper_name    VARCHAR2(50);
    -- Variable to store the substring
    first_word    VARCHAR2(20);
BEGIN
    -- Converting to uppercase
    upper_name := UPPER(customer_name);
    DBMS_OUTPUT.PUT_LINE('Uppercase Name: ' || upper_name);

    -- Extracting a substring
    first_word := SUBSTR(customer_name, 1, INSTR(customer_name, ' ') - 1);
    DBMS_OUTPUT.PUT_LINE('First Word: ' || first_word);
END;
/

Explanation This example showcases common string manipulation functions like UPPER and SUBSTR with VARCHAR2, which are frequently used for data cleaning, formatting, and extraction in PL/SQL.

 

Example 3: Handling NULL and Empty Strings in VARCHAR2

DECLARE
    -- Declaring a VARCHAR2 variable with a NULL value
    middle_initial VARCHAR2(1);
    -- Declaring a VARCHAR2 variable as an empty string (treated as NULL in Oracle)
    empty_string VARCHAR2(10) := '';
BEGIN
    -- NULL check
    IF middle_initial IS NULL THEN
        DBMS_OUTPUT.PUT_LINE('Middle Initial is NULL.');
    END IF;

    -- Empty string check (Oracle treats empty strings as NULL for VARCHAR2)
    IF empty_string IS NULL THEN
        DBMS_OUTPUT.PUT_LINE('Empty string is treated as NULL in Oracle VARCHAR2.');
    END IF;

    -- Assigning a value
    middle_initial := 'J';
    IF middle_initial IS NOT NULL THEN
        DBMS_OUTPUT.PUT_LINE('Middle Initial is now: ' || middle_initial);
    END IF;
END;
/

Explanation This example demonstrates Oracle's handling of NULL and empty strings for VARCHAR2. In Oracle PL/SQL, an empty string ('') is treated as NULL for VARCHAR2 and CHAR data types, a crucial detail for data validation and conditional logic.

 

Example 4: VARCHAR2 for Dynamic SQL (Advanced)

DECLARE
    -- Variable to store a table name
    table_name VARCHAR2(30) := 'EMPLOYEES';
    -- Variable to store a column name
    column_to_check VARCHAR2(30) := 'JOB_ID';
    -- Variable to store the dynamic query string
    dynamic_query VARCHAR2(200);
    -- Variable to store the result of the query
    distinct_job_id VARCHAR2(10);
BEGIN
    -- Constructing a dynamic SQL query
    dynamic_query := 'SELECT ' || column_to_check || ' FROM ' || table_name || ' WHERE ROWNUM = 1';

    -- Executing the dynamic query
    EXECUTE IMMEDIATE dynamic_query INTO distinct_job_id;

    DBMS_OUTPUT.PUT_LINE('First Job ID from ' || table_name || ': ' || distinct_job_id);
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error executing dynamic query: ' || SQLERRM);
END;
/

Explanation This advanced example shows VARCHAR2 being used to construct dynamic SQL statements. This is a powerful technique for building flexible and adaptable PL/SQL procedures, allowing you to modify SQL commands at runtime.

 

Example 5: VARCHAR2 for Regular Expressions (Advanced)

DECLARE
    -- String containing a sample email address
    email_address VARCHAR2(100) := 'user.name@example.com';
    -- Regular expression pattern to validate an email format
    email_pattern CONSTANT VARCHAR2(100) := '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$';
    -- Variable to hold the extracted domain
    domain_name VARCHAR2(50);
BEGIN
    -- Using REGEXP_LIKE to validate email format
    IF REGEXP_LIKE(email_address, email_pattern) THEN
        DBMS_OUTPUT.PUT_LINE('Email address is valid: ' || email_address);
    ELSE
        DBMS_OUTPUT.PUT_LINE('Email address is invalid: ' || email_address);
    END IF;

    -- Using REGEXP_SUBSTR to extract the domain name
    domain_name := REGEXP_SUBSTR(email_address, '@([a-zA-Z0-9.-]+)\.', 1, 1, NULL, 1);
    DBMS_OUTPUT.PUT_LINE('Domain Name: ' || domain_name);
END;
/

Explanation This advanced example demonstrates the use of VARCHAR2 in conjunction with Oracle's powerful regular expression functions (REGEXP_LIKE, REGEXP_SUBSTR). This is essential for advanced string parsing, validation, and data extraction from complex textual data.

 

DATE

The DATE data type stores date and time information, including year, month, day, hour, minute, and second. It's fundamental for handling temporal data in Oracle PL/SQL.

 

Example 1: Declaring a Simple DATE Variable and SYSDATE

DECLARE
    -- Declaring a date variable
    current_date DATE;
BEGIN
    -- Assigning the current system date and time
    current_date := SYSDATE;
    DBMS_OUTPUT.PUT_LINE('Current Date and Time: ' || TO_CHAR(current_date, 'YYYY-MM-DD HH24:MI:SS'));
END;
/

Explanation This basic example shows how to declare a DATE variable and assign the current system date and time using SYSDATE. It also uses TO_CHAR for formatted date output, which is crucial for displaying dates in a human-readable format.

 

Example 2: DATE Arithmetic (Adding/Subtracting Days)

DECLARE
    -- Declaring a start date
    start_date DATE := TO_DATE('2025-01-01', 'YYYY-MM-DD');
    -- Variable to store a date after adding days
    future_date DATE;
    -- Variable to store a date after subtracting days
    past_date   DATE;
BEGIN
    -- Adding 10 days to the start date
    future_date := start_date + 10;
    DBMS_OUTPUT.PUT_LINE('Start Date: ' || TO_CHAR(start_date, 'YYYY-MM-DD'));
    DBMS_OUTPUT.PUT_LINE('Date 10 days later: ' || TO_CHAR(future_date, 'YYYY-MM-DD'));

    -- Subtracting 5 days from the start date
    past_date := start_date - 5;
    DBMS_OUTPUT.PUT_LINE('Date 5 days earlier: ' || TO_CHAR(past_date, 'YYYY-MM-DD'));
END;
/

Explanation This example demonstrates basic date arithmetic with the DATE data type, showing how to add and subtract days to manipulate dates, a common requirement in scheduling and reporting applications.

 

Example 3: DATE Functions (MONTHS_BETWEEN, ADD_MONTHS)

DECLARE
    -- Declaring two date variables
    hire_date DATE := TO_DATE('2020-03-15', 'YYYY-MM-DD');
    current_time DATE := SYSDATE;
    -- Variable to store the number of months between dates
    months_diff NUMBER;
    -- Variable to store a date after adding months
    future_month_date DATE;
BEGIN
    -- Calculating the number of months between two dates
    months_diff := MONTHS_BETWEEN(current_time, hire_date);
    DBMS_OUTPUT.PUT_LINE('Months between hire date and now: ' || ROUND(months_diff, 2));

    -- Adding 6 months to a date
    future_month_date := ADD_MONTHS(hire_date, 6);
    DBMS_OUTPUT.PUT_LINE('Hire Date: ' || TO_CHAR(hire_date, 'YYYY-MM-DD'));
    DBMS_OUTPUT.PUT_LINE('Date after 6 months: ' || TO_CHAR(future_month_date, 'YYYY-MM-DD'));
END;
/

Explanation This example introduces MONTHS_BETWEEN and ADD_MONTHS, powerful functions for performing more complex date manipulations, such as calculating age in months or determining future dates.

 

Example 4: Handling Time Zones with DATE (Advanced)

DECLARE
    -- Declaring a DATE variable
    event_time DATE := TO_DATE('2025-06-14 10:00:00', 'YYYY-MM-DD HH24:MI:SS');
BEGIN
    -- DATE data type does not store time zone information directly.
    -- For time zone handling, consider TIMESTAMP WITH TIME ZONE.
    DBMS_OUTPUT.PUT_LINE('Event Time (Local): ' || TO_CHAR(event_time, 'YYYY-MM-DD HH24:MI:SS'));

    -- To simulate time zone awareness with DATE, you'd convert to UTC or a specific time zone
    -- and back, typically involving database session settings or explicit conversions.
    -- Example: Converting a local time to a UTC time (simplistic, assumes session timezone)
    DBMS_OUTPUT.PUT_LINE('Event Time in UTC (estimated): ' || TO_CHAR(NEW_TIME(event_time, 'PDT', 'GMT'), 'YYYY-MM-DD HH24:MI:SS'));
END;
/

Explanation This example touches upon the limitations of DATE concerning time zones and suggests using TIMESTAMP WITH TIME ZONE for robust time zone handling, an important consideration for global applications. It also briefly shows NEW_TIME for basic time zone conversions.

 

Example 5: Working with Date Literals and Comparisons (Advanced)

DECLARE
    -- A date literal for a project deadline
    project_deadline DATE := '2025-12-31';
    -- Current date for comparison
    today_date DATE := TRUNC(SYSDATE); -- TRUNC removes the time component
BEGIN
    DBMS_OUTPUT.PUT_LINE('Project Deadline: ' || TO_CHAR(project_deadline, 'YYYY-MM-DD'));
    DBMS_OUTPUT.PUT_LINE('Today''s Date: ' || TO_CHAR(today_date, 'YYYY-MM-DD'));

    IF today_date > project_deadline THEN
        DBMS_OUTPUT.PUT_LINE('Project is overdue!');
    ELSIF today_date = project_deadline THEN
        DBMS_OUTPUT.PUT_LINE('Project deadline is today!');
    ELSE
        DBMS_OUTPUT.PUT_LINE('Project is still on track.');
    END IF;

    -- Calculate days remaining
    DBMS_OUTPUT.PUT_LINE('Days remaining to deadline: ' || (project_deadline - today_date));
END;
/

Explanation This advanced example demonstrates using date literals, comparing DATE values, and calculating the difference between two dates (which results in the number of days). This is fundamental for project management, scheduling, and general temporal comparisons in PL/SQL.

 

BOOLEAN

The BOOLEAN data type stores logical values: TRUE, FALSE, or NULL. It's primarily used for conditional logic and flag variables in PL/SQL.

 

Example 1: Declaring a Simple BOOLEAN

DECLARE
    -- Declaring a boolean variable for a flag
    is_processed BOOLEAN := FALSE;
BEGIN
    -- Conditional check
    IF NOT is_processed THEN
        DBMS_OUTPUT.PUT_LINE('Data not yet processed.');
        is_processed := TRUE; -- Change the value
    END IF;

    IF is_processed THEN
        DBMS_OUTPUT.PUT_LINE('Data has been processed.');
    END IF;
END;
/

Explanation This basic example illustrates the declaration and simple usage of a BOOLEAN variable, demonstrating its role in controlling program flow through IF statements. This is fundamental for logical operations in PL/SQL.

 

Example 2: BOOLEAN in Logical Operations (AND, OR, NOT)

DECLARE
    -- Boolean flags for user permissions
    can_read  BOOLEAN := TRUE;
    can_write BOOLEAN := FALSE;
    can_delete BOOLEAN := TRUE;
BEGIN
    -- Using AND operator
    IF can_read AND can_write THEN
        DBMS_OUTPUT.PUT_LINE('User has read and write access.');
    ELSE
        DBMS_OUTPUT.PUT_LINE('User does not have both read and write access.');
    END IF;

    -- Using OR operator
    IF can_read OR can_delete THEN
        DBMS_OUTPUT.PUT_LINE('User has either read or delete access (or both).');
    END IF;

    -- Using NOT operator
    IF NOT can_write THEN
        DBMS_OUTPUT.PUT_LINE('User cannot write.');
    END IF;
END;
/

Explanation This example demonstrates BOOLEAN variables in conjunction with logical operators (AND, OR, NOT), which are essential for building complex conditional expressions and controlling program flow in PL/SQL.

 

Example 3: Returning BOOLEAN from a Function (Advanced)

CREATE OR REPLACE FUNCTION is_valid_email (p_email_address IN VARCHAR2)
    RETURN BOOLEAN
IS
    -- Simple pattern for demonstration
    v_pattern CONSTANT VARCHAR2(100) := '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$';
BEGIN
    -- Return TRUE if the email matches the pattern, otherwise FALSE
    RETURN REGEXP_LIKE(p_email_address, v_pattern);
EXCEPTION
    WHEN OTHERS THEN
        -- Handle potential errors, though REGEXP_LIKE usually safe
        RETURN FALSE;
END;
/

DECLARE
    email1 VARCHAR2(100) := 'test@example.com';
    email2 VARCHAR2(100) := 'invalid-email';
BEGIN
    IF is_valid_email(email1) THEN
        DBMS_OUTPUT.PUT_LINE('"' || email1 || '" is a valid email address.');
    ELSE
        DBMS_OUTPUT.PUT_LINE('"' || email1 || '" is NOT a valid email address.');
    END IF;

    IF is_valid_email(email2) THEN
        DBMS_OUTPUT.PUT_LINE('"' || email2 || '" is a valid email address.');
    ELSE
        DBMS_OUTPUT.PUT_LINE('"' || email2 || '" is NOT a valid email address.');
    END IF;
END;
/

Explanation This advanced example demonstrates returning a BOOLEAN value from a PL/SQL function. This is a common pattern for creating reusable validation logic or flag-checking functions, improving modularity in your Oracle PL/SQL applications.

 

Example 4: BOOLEAN for Loop Control (Advanced)

DECLARE
    -- Boolean flag to control loop execution
    stop_loop BOOLEAN := FALSE;
    counter   NUMBER := 0;
BEGIN
    -- Loop until stop_loop becomes TRUE
    WHILE NOT stop_loop LOOP
        counter := counter + 1;
        DBMS_OUTPUT.PUT_LINE('Loop iteration: ' || counter);

        IF counter >= 5 THEN
            stop_loop := TRUE; -- Set flag to exit loop
        END IF;
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('Loop finished.');
END;
/

Explanation This example illustrates using a BOOLEAN variable to control the execution of a WHILE loop. This pattern is often used when the exit condition of a loop depends on a complex logical state.

 

Example 5: BOOLEAN for Feature Toggling (Advanced)

DECLARE
    -- A boolean constant to control a feature
    FEATURE_A_ENABLED CONSTANT BOOLEAN := TRUE;
    -- Another feature toggle
    FEATURE_B_ENABLED CONSTANT BOOLEAN := FALSE;
BEGIN
    IF FEATURE_A_ENABLED THEN
        DBMS_OUTPUT.PUT_LINE('Feature A is enabled. Executing Feature A logic...');
        -- Call a procedure for Feature A
        -- process_feature_a();
    ELSE
        DBMS_OUTPUT.PUT_LINE('Feature A is disabled.');
    END IF;

    IF FEATURE_B_ENABLED THEN
        DBMS_OUTPUT.PUT_LINE('Feature B is enabled. Executing Feature B logic...');
        -- process_feature_b();
    ELSE
        DBMS_OUTPUT.PUT_LINE('Feature B is disabled.');
    END IF;
END;
/

Explanation This advanced example demonstrates using BOOLEAN constants for "feature toggling," a powerful technique in software development to enable or disable functionalities without code redeployment, providing flexibility for A/B testing or staged rollouts.


 

 

Declaring Variables and Constants

Declaring variables and constants in Oracle PL/SQL involves specifying their name, data type, and optionally an initial value. Proper declaration ensures that your program allocates the necessary memory and correctly interprets the values stored.

 

Example 1: Basic Variable Declaration

DECLARE
    -- Declare a simple integer variable
    v_count NUMBER;
    -- Declare a string variable
    v_message VARCHAR2(255);
BEGIN
    -- Assign values to the declared variables
    v_count := 10;
    v_message := 'Hello, PL/SQL World!';

    DBMS_OUTPUT.PUT_LINE('Count: ' || v_count);
    DBMS_OUTPUT.PUT_LINE('Message: ' || v_message);
END;
/

Explanation This example demonstrates the most straightforward way to declare variables in the DECLARE section of a PL/SQL block. It covers basic NUMBER and VARCHAR2 declarations, which are fundamental for storing data.

 

Example 2: Declaring Variables with Initial Values

DECLARE
    -- Declare and initialize a numeric variable
    v_total_sum NUMBER := 0;
    -- Declare and initialize a boolean flag
    v_is_valid BOOLEAN := FALSE;
    -- Declare and initialize a date variable to current date
    v_start_date DATE := SYSDATE;
BEGIN
    DBMS_OUTPUT.PUT_LINE('Initial Sum: ' || v_total_sum);
    DBMS_OUTPUT.PUT_LINE('Is Valid: ' || CASE WHEN v_is_valid THEN 'TRUE' ELSE 'FALSE' END);
    DBMS_OUTPUT.PUT_LINE('Start Date: ' || TO_CHAR(v_start_date, 'YYYY-MM-DD'));
END;
/

Explanation This example shows how to declare variables and assign them an initial value directly at the time of declaration. This is a good practice for ensuring variables have a known state from the start, improving code reliability.

 

Example 3: Declaring Constants

DECLARE
    -- Declare a constant for the maximum length of a name
    MAX_NAME_LENGTH CONSTANT NUMBER := 50;
    -- Declare a constant for a fixed discount rate
    DISCOUNT_RATE CONSTANT NUMBER(3, 2) := 0.15; -- 15%
    -- Declare a constant for the company name
    COMPANY_NAME CONSTANT VARCHAR2(100) := 'Acme Corporation';
BEGIN
    DBMS_OUTPUT.PUT_LINE('Max Name Length: ' || MAX_NAME_LENGTH);
    DBMS_OUTPUT.PUT_LINE('Discount Rate: ' || DISCOUNT_RATE * 100 || '%');
    DBMS_OUTPUT.PUT_LINE('Company: ' || COMPANY_NAME);

    -- Attempting to modify a constant will raise a PLS-00363 error.
    -- MAX_NAME_LENGTH := 60;
END;
/

Explanation This example focuses on declaring constants using the CONSTANT keyword. It reinforces that constant values are immutable after declaration, making them ideal for fixed application parameters.

 

Example 4: Declaring Variables in Subprograms (Advanced)

DECLARE
    -- Global variable for the block
    global_counter NUMBER := 0;

    -- Procedure to increment a counter
    PROCEDURE increment_counter (p_step IN NUMBER) IS
        -- Local variable to the procedure
        procedure_local_var NUMBER := 100;
    BEGIN
        global_counter := global_counter + p_step;
        DBMS_OUTPUT.PUT_LINE('Inside procedure - Global Counter: ' || global_counter);
        DBMS_OUTPUT.PUT_LINE('Inside procedure - Local Var: ' || procedure_local_var);
    END increment_counter;

    -- Function to double a number
    FUNCTION double_number (p_input IN NUMBER) RETURN NUMBER IS
        -- Local constant to the function
        MULTIPLIER CONSTANT NUMBER := 2;
    BEGIN
        RETURN p_input * MULTIPLIER;
    END double_number;

BEGIN
    DBMS_OUTPUT.PUT_LINE('Initial Global Counter: ' || global_counter);
    increment_counter(5);
    increment_counter(3);
    DBMS_OUTPUT.PUT_LINE('Final Global Counter: ' || global_counter);
    DBMS_OUTPUT.PUT_LINE('Doubled 7: ' || double_number(7));
END;
/

Explanation This advanced example demonstrates declaring variables and constants within PL/SQL subprograms (procedures and functions). It highlights the concept of variable scope, where variables declared inside a subprogram are local to that subprogram.

 

Example 5: Declaring Collection Variables (Advanced)

DECLARE
    -- Declaring a PL/SQL table (associative array) of numbers
    TYPE number_list_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
    my_numbers number_list_type;

    -- Declaring a nested table of VARCHAR2
    TYPE string_array_type IS TABLE OF VARCHAR2(50);
    my_strings string_array_type := string_array_type(); -- Initialize for nested table

    -- Declaring a record type
    TYPE employee_rec_type IS RECORD (
        employee_id NUMBER,
        first_name VARCHAR2(50),
        salary NUMBER(8,2)
    );
    my_employee employee_rec_type;
BEGIN
    -- Populate the PL/SQL table
    my_numbers(1) := 10;
    my_numbers(2) := 20;
    my_numbers(10) := 100; -- Sparse array

    DBMS_OUTPUT.PUT_LINE('First number: ' || my_numbers(1));
    DBMS_OUTPUT.PUT_LINE('Tenth number: ' || my_numbers(10));

    -- Populate the nested table
    my_strings.EXTEND;
    my_strings(my_strings.LAST) := 'Apple';
    my_strings.EXTEND;
    my_strings(my_strings.LAST) := 'Banana';

    DBMS_OUTPUT.PUT_LINE('First string: ' || my_strings(1));

    -- Populate the record
    my_employee.employee_id := 101;
    my_employee.first_name := 'Jane';
    my_employee.salary := 75000.50;

    DBMS_OUTPUT.PUT_LINE('Employee: ' || my_employee.first_name || ' (ID: ' || my_employee.employee_id || ') - Salary: ' || my_employee.salary);
END;
/

Explanation This advanced example delves into declaring more complex data structures: PL/SQL tables (associative arrays), nested tables, and record types. These are essential for handling collections of data and structured information within your PL/SQL programs.


 

 

The %TYPE Attribute (Anchoring to table columns)

The %TYPE attribute in Oracle PL/SQL is a powerful tool for declaring variables that inherit the data type and size (and sometimes constraints) of a specific database column or another already declared variable. This "anchoring" makes your code more robust and adaptable to schema changes. When the underlying column's data type or size changes, your PL/SQL code automatically adjusts without needing modification, reducing maintenance efforts.

 

Example 1: Basic %TYPE Declaration

DECLARE
    -- Declaring a variable v_employee_name with the same data type and size
    -- as the first_name column in the employees table.
    -- This is ideal for ensuring data type consistency with your database schema.
    v_employee_name employees.first_name%TYPE;
    v_employee_id   employees.employee_id%TYPE;
BEGIN
    -- Assigning a value to the anchored variable
    v_employee_name := 'Stephen';
    v_employee_id := 100; -- Assuming employee_id 100 exists

    DBMS_OUTPUT.PUT_LINE('Employee Name (anchored): ' || v_employee_name);
    DBMS_OUTPUT.PUT_LINE('Employee ID (anchored): ' || v_employee_id);
END;
/

Explanation This beginner example demonstrates the fundamental use of %TYPE to declare variables that derive their data type and length directly from existing table columns. This automatically adapts your variable declarations to any changes in the underlying table structure.

 

Example 2: %TYPE with SELECT INTO

DECLARE
    -- Anchoring a variable to the employees.salary column
    emp_salary employees.salary%TYPE;
    -- Anchoring a variable to the departments.department_name column
    dept_name  departments.department_name%TYPE;
BEGIN
    -- Retrieving salary for a specific employee into the anchored variable
    SELECT salary
    INTO emp_salary
    FROM employees
    WHERE employee_id = 101; -- Assuming employee_id 101 exists

    DBMS_OUTPUT.PUT_LINE('Employee 101 Salary: ' || emp_salary);

    -- Retrieving department name for a specific department
    SELECT department_name
    INTO dept_name
    FROM departments
    WHERE department_id = 10; -- Assuming department_id 10 exists

    DBMS_OUTPUT.PUT_LINE('Department 10 Name: ' || dept_name);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('No data found for the specified ID.');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/

Explanation This example combines %TYPE with SELECT INTO statements, a very common and efficient way to fetch data from database tables directly into PL/SQL variables while maintaining data type consistency. This reduces potential data truncation issues.

 

Example 3: %TYPE for Function Parameters and Return Types (Advanced)

-- Function to get an employee's email, anchored to the email column type
CREATE OR REPLACE FUNCTION get_employee_email (
    p_employee_id IN employees.employee_id%TYPE
) RETURN employees.email%TYPE
IS
    v_email employees.email%TYPE;
BEGIN
    SELECT email
    INTO v_email
    FROM employees
    WHERE employee_id = p_employee_id;

    RETURN v_email;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
END;
/

DECLARE
    -- Declaring a variable to hold the retrieved email, also anchored
    email_address employees.email%TYPE;
    emp_id        employees.employee_id%TYPE := 103; -- Assuming employee_id 103 exists
BEGIN
    email_address := get_employee_email(emp_id);

    IF email_address IS NOT NULL THEN
        DBMS_OUTPUT.PUT_LINE('Employee ' || emp_id || ' Email: ' || email_address);
    ELSE
        DBMS_OUTPUT.PUT_LINE('Email not found for employee ' || emp_id);
    END IF;
END;
/

Explanation This advanced example demonstrates using %TYPE for both function parameters and return types. This is a best practice for building robust and reusable PL/SQL functions and procedures, as it ensures that the function's interface remains consistent with the underlying database schema.

 

Example 4: %TYPE with a Previously Declared Variable (Advanced)

DECLARE
    -- Declare a base variable
    v_product_code VARCHAR2(20);
    -- Declare another variable inheriting the data type and size from v_product_code
    v_new_product_code v_product_code%TYPE;
    -- Declare a third variable inheriting from the base variable
    v_old_product_code v_product_code%TYPE;
BEGIN
    v_product_code := 'ABC-12345';
    v_new_product_code := 'XYZ-98765';
    v_old_product_code := v_product_code;

    DBMS_OUTPUT.PUT_LINE('Original Product Code: ' || v_product_code);
    DBMS_OUTPUT.PUT_LINE('New Product Code (anchored to original var): ' || v_new_product_code);
    DBMS_OUTPUT.PUT_LINE('Old Product Code (anchored to original var): ' || v_old_product_code);

    -- Demonstrate that if v_product_code's length were to change (e.g., in a package),
    -- v_new_product_code would automatically adapt.
END;
/

Explanation This example illustrates a less common but equally valid use of %TYPE: deriving the data type from another already declared PL/SQL variable. This can be useful for maintaining consistency within a PL/SQL block or package when variables are logically related.

 

Example 5: %TYPE for Collection Elements (Advanced)

DECLARE
    -- Define a PL/SQL table (associative array) type where each element
    -- is anchored to the employees.last_name column type.
    TYPE employee_names_tab_type IS TABLE OF employees.last_name%TYPE INDEX BY PLS_INTEGER;

    -- Declare a variable of the defined collection type
    employee_last_names employee_names_tab_type;

    -- Cursor to fetch employee last names
    CURSOR c_employees IS
        SELECT last_name FROM employees WHERE ROWNUM <= 5 ORDER BY last_name;

    i PLS_INTEGER := 1;
BEGIN
    -- Populate the collection using a cursor loop
    FOR emp_rec IN c_employees LOOP
        employee_last_names(i) := emp_rec.last_name;
        i := i + 1;
    END LOOP;

    -- Display the contents of the collection
    DBMS_OUTPUT.PUT_LINE('First 5 Employee Last Names:');
    FOR j IN 1..employee_last_names.COUNT LOOP
        DBMS_OUTPUT.PUT_LINE('- ' || employee_last_names(j));
    END LOOP;
END;
/

Explanation This advanced example demonstrates using %TYPE to define the data type of elements within a PL/SQL collection (specifically an associative array). This ensures that each element in the collection has the same data type as the specified database column, maintaining consistency when handling sets of related data.


 

The %ROWTYPE Attribute (Anchoring to table rows)

The %ROWTYPE attribute is an extremely powerful feature in Oracle PL/SQL that allows you to declare a record variable that can hold an entire row of data from a specified database table or view, or even from a cursor. This attribute automatically defines a record with fields corresponding to each column in the row, inheriting their respective data types and sizes. This eliminates the need to declare individual variables for each column, making your code significantly more concise, readable, and robust against schema changes.

 

Example 1: Basic %ROWTYPE Declaration from a Table

DECLARE
    -- Declaring a record variable that can hold an entire row from the employees table.
    -- This is highly recommended for fetching full rows of data.
    employee_record employees%ROWTYPE;
BEGIN
    -- Fetching a full row for employee_id 100 into the record variable
    SELECT *
    INTO employee_record
    FROM employees
    WHERE employee_id = 100; -- Assuming employee_id 100 exists in your HR schema

    -- Accessing individual fields of the record using dot notation
    DBMS_OUTPUT.PUT_LINE('Employee ID: ' || employee_record.employee_id);
    DBMS_OUTPUT.PUT_LINE('First Name: ' || employee_record.first_name);
    DBMS_OUTPUT.PUT_LINE('Last Name: ' || employee_record.last_name);
    DBMS_OUTPUT.PUT_LINE('Salary: ' || employee_record.salary);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Employee 100 not found.');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/

Explanation This beginner-friendly example demonstrates the core functionality of %ROWTYPE. It shows how to declare a record variable that automatically mirrors the structure of the employees table, allowing you to fetch an entire row and access individual columns using dot notation (record_name.column_name).

 

Example 2: %ROWTYPE with Cursors for Iterating Through Rows

DECLARE
    -- Declaring a cursor to fetch employees earning more than 10000
    CURSOR c_high_earners IS
        SELECT employee_id, first_name, last_name, salary
        FROM employees
        WHERE salary > 10000
        ORDER BY salary DESC;

    -- Declaring a record variable based on the cursor's structure.
    -- This ensures the record matches the columns selected by the cursor.
    high_earner_rec c_high_earners%ROWTYPE;
BEGIN
    DBMS_OUTPUT.PUT_LINE('High Earners (Salary > 10000):');
    DBMS_OUTPUT.PUT_LINE('-----------------------------');

    -- Opening the cursor
    OPEN c_high_earners;

    LOOP
        -- Fetching a row into the %ROWTYPE variable
        FETCH c_high_earners INTO high_earner_rec;

        -- Exiting the loop when no more rows are found
        EXIT WHEN c_high_earners%NOTFOUND;

        DBMS_OUTPUT.PUT_LINE('ID: ' || high_earner_rec.employee_id ||
                             ', Name: ' || high_earner_rec.first_name || ' ' || high_earner_rec.last_name ||
                             ', Salary: ' || high_earner_rec.salary);
    END LOOP;

    -- Closing the cursor
    CLOSE c_high_earners;
END;
/

Explanation This example demonstrates a common and powerful pattern: using %ROWTYPE in conjunction with explicit cursors. The record variable high_earner_rec automatically adopts the structure of the SELECT statement in c_high_earners, simplifying iteration through result sets.

 

Example 3: %ROWTYPE in Cursor FOR Loops (Advanced)

DECLARE
    -- No explicit record declaration needed for the FOR loop.
BEGIN
    DBMS_OUTPUT.PUT_LINE('Employees in Department 50:');
    DBMS_OUTPUT.PUT_LINE('-------------------------');

    -- The record variable emp_rec is implicitly declared as employees%ROWTYPE
    -- by the Cursor FOR loop, for each row returned by the query.
    FOR emp_rec IN (SELECT * FROM employees WHERE department_id = 50 ORDER BY last_name) LOOP
        DBMS_OUTPUT.PUT_LINE('  ID: ' || emp_rec.employee_id ||
                             ', Name: ' || emp_rec.first_name || ' ' || emp_rec.last_name ||
                             ', Job: ' || emp_rec.job_id);
    END LOOP;

    DBMS_OUTPUT.PUT_LINE('-------------------------');
    DBMS_OUTPUT.PUT_LINE('Departments and Managers:');
    FOR dept_mgr_rec IN (
        SELECT d.department_name, e.first_name || ' ' || e.last_name AS manager_name
        FROM departments d
        LEFT JOIN employees e ON d.manager_id = e.employee_id
        ORDER BY d.department_name
    ) LOOP
        DBMS_OUTPUT.PUT_LINE('  Department: ' || dept_mgr_rec.department_name ||
                             ', Manager: ' || NVL(dept_mgr_rec.manager_name, 'N/A'));
    END LOOP;
END;
/

Explanation This advanced example highlights the elegance of the Cursor FOR loop with %ROWTYPE. The loop automatically declares a record variable (e.g., emp_rec) that matches the structure of the SELECT statement, simplifying row processing and eliminating explicit cursor management. This is often the preferred method for iterating through query results.

 

Example 4: %ROWTYPE for Inserting/Updating Rows (Advanced)

DECLARE
    -- Declaring a record variable anchored to the employees table structure
    new_employee_rec employees%ROWTYPE;
BEGIN
    -- Populate the record with values for a new employee
    new_employee_rec.employee_id := 999;
    new_employee_rec.first_name := 'Test';
    new_employee_rec.last_name := 'User';
    new_employee_rec.email := 'TESTUSER'; -- Must be unique
    new_employee_rec.phone_number := '515.123.4567';
    new_employee_rec.hire_date := SYSDATE;
    new_employee_rec.job_id := 'IT_PROG';
    new_employee_rec.salary := 6000;
    new_employee_rec.commission_pct := NULL;
    new_employee_rec.manager_id := 103; -- Assuming 103 is a valid manager
    new_employee_rec.department_id := 60; -- Assuming 60 is a valid department

    -- Inserting the entire record into the employees table
    -- This syntax simplifies INSERT statements.
    INSERT INTO employees VALUES new_employee_rec;

    DBMS_OUTPUT.PUT_LINE('New employee inserted: ' || new_employee_rec.first_name || ' ' || new_employee_rec.last_name);

    -- Updating a specific column of the record and then updating the table
    new_employee_rec.salary := 6500; -- Change salary in the record
    UPDATE employees
    SET ROW = new_employee_rec -- Update the entire row with the record's current values
    WHERE employee_id = new_employee_rec.employee_id;

    DBMS_OUTPUT.PUT_LINE('Updated salary for employee ' || new_employee_rec.employee_id);

    -- Rollback the transaction to keep the table clean for repeated execution
    ROLLBACK;
    DBMS_OUTPUT.PUT_LINE('Transaction rolled back.');
EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        DBMS_OUTPUT.PUT_LINE('Error: Employee ID or Email already exists. Rolling back.');
        ROLLBACK;
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('An error occurred during DML: ' || SQLERRM);
        ROLLBACK;
END;
/

Explanation This advanced example showcases using %ROWTYPE not just for fetching, but also for INSERTING and UPDATING entire rows. By manipulating a %ROWTYPE variable and then performing DML operations using VALUES record_name or SET ROW = record_name, you simplify your SQL statements and maintain consistency with the table structure.

 

Example 5: %ROWTYPE with Record Collections (Advanced)

DECLARE
    -- Define a PL/SQL table (nested table) of employee records
    TYPE employee_tab_type IS TABLE OF employees%ROWTYPE;

    -- Declare a variable of the defined collection type
    hr_employees employee_tab_type := employee_tab_type();

    -- Cursor to fetch a subset of employees
    CURSOR c_some_employees IS
        SELECT *
        FROM employees
        WHERE ROWNUM <= 3
        ORDER BY employee_id;
BEGIN
    -- Populate the collection by fetching rows from the cursor
    FOR emp_row IN c_some_employees LOOP
        hr_employees.EXTEND; -- Add a new element to the collection
        hr_employees(hr_employees.LAST) := emp_row; -- Assign the fetched row to the new element
    END LOOP;

    DBMS_OUTPUT.PUT_LINE('Employees in Collection:');
    DBMS_OUTPUT.PUT_LINE('-----------------------');

    -- Iterate through the collection and display employee details
    FOR i IN 1..hr_employees.COUNT LOOP
        DBMS_OUTPUT.PUT_LINE('  ID: ' || hr_employees(i).employee_id ||
                             ', Name: ' || hr_employees(i).first_name || ' ' || hr_employees(i).last_name ||
                             ', Salary: ' || hr_employees(i).salary);
    END LOOP;

    -- Accessing a specific element in the collection
    IF hr_employees.COUNT > 1 THEN
        DBMS_OUTPUT.PUT_LINE('Second Employee Last Name: ' || hr_employees(2).last_name);
    END IF;
END;
/

Explanation This advanced example demonstrates combining %ROWTYPE with PL/SQL collections (specifically a nested table of records). This allows you to store and manipulate entire sets of database rows in memory as a single logical unit, which is highly useful for batch processing, temporary storage, or passing multiple rows between PL/SQL subprograms.


 

 

Bind Variables

Bind variables are placeholders in SQL statements or PL/SQL blocks that allow you to substitute values at runtime. Instead of embedding literal values directly into your SQL statements, you use bind variables, which are then passed to the SQL engine. This offers significant advantages: enhanced performance through statement caching (the SQL engine can reuse the execution plan for repeated statements), improved security by preventing SQL injection, and better resource utilization.

 

Example 1: Basic Bind Variable in an Anonymous Block

DECLARE
    -- Declare a PL/SQL variable
    p_employee_id NUMBER := 101;
    v_employee_name employees.first_name%TYPE;
BEGIN
    -- SQL statement using a bind variable (:p_employee_id)
    -- This allows the SQL engine to parse and optimize the query once.
    SELECT first_name
    INTO v_employee_name
    FROM employees
    WHERE employee_id = :p_employee_id; -- :p_employee_id is the bind variable

    DBMS_OUTPUT.PUT_LINE('Employee ID ' || p_employee_id || ' First Name: ' || v_employee_name);

    -- Change the value of the PL/SQL variable and re-execute
    p_employee_id := 102;
    SELECT first_name
    INTO v_employee_name
    FROM employees
    WHERE employee_id = :p_employee_id;
    DBMS_OUTPUT.PUT_LINE('Employee ID ' || p_employee_id || ' First Name: ' || v_employee_name);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('No employee found for the given ID.');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/

Explanation This fundamental example shows how to use a bind variable (:p_employee_id) within a SELECT statement in an anonymous PL/SQL block. The value from the PL/SQL variable p_employee_id is "bound" to the placeholder, allowing Oracle to reuse the parsed SQL statement for different employee IDs.

 

Example 2: Bind Variables in Dynamic SQL (EXECUTE IMMEDIATE)

DECLARE
    -- PL/SQL variables
    table_name_var VARCHAR2(30) := 'EMPLOYEES';
    column_name_var VARCHAR2(30) := 'SALARY';
    min_salary_param NUMBER := 7000;
    max_salary_param NUMBER := 12000;
    employee_count   NUMBER;
    dynamic_sql_stmt VARCHAR2(500);
BEGIN
    -- Constructing a dynamic SQL statement with bind variable placeholders
    dynamic_sql_stmt := 'SELECT COUNT(*) FROM ' || table_name_var ||
                        ' WHERE ' || column_name_var || ' BETWEEN :min_sal AND :max_sal';

    -- Executing the dynamic SQL using EXECUTE IMMEDIATE and
    -- passing the bind variables using the USING clause.
    EXECUTE IMMEDIATE dynamic_sql_stmt
    INTO employee_count
    USING min_salary_param, max_salary_param;

    DBMS_OUTPUT.PUT_LINE('Number of employees with salary between ' || min_salary_param || ' and ' || max_salary_param || ': ' || employee_count);

    -- Change the bind variable values and re-execute
    min_salary_param := 15000;
    max_salary_param := 20000;
    EXECUTE IMMEDIATE dynamic_sql_stmt
    INTO employee_count
    USING min_salary_param, max_salary_param;

    DBMS_OUTPUT.PUT_LINE('Number of employees with salary between ' || min_salary_param || ' and ' || max_salary_param || ': ' || employee_count);

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error in dynamic SQL with bind variables: ' || SQLERRM);
END;
/

Explanation This example demonstrates the critical use of bind variables with EXECUTE IMMEDIATE for dynamic SQL. The USING clause is employed to pass values to the named bind variables (:min_sal, :max_sal), ensuring statement reuse and protecting against SQL injection, a core aspect of secure PL/SQL programming.

 

Example 3: Bind Variables in Procedures/Functions (IN parameters)

CREATE OR REPLACE PROCEDURE get_employee_details (
    p_emp_id   IN employees.employee_id%TYPE,
    p_first_name OUT employees.first_name%TYPE,
    p_last_name  OUT employees.last_name%TYPE,
    p_salary     OUT employees.salary%TYPE
)
IS
BEGIN
    -- The IN parameter p_emp_id acts as a bind variable in the SQL query
    SELECT first_name, last_name, salary
    INTO p_first_name, p_last_name, p_salary
    FROM employees
    WHERE employee_id = p_emp_id;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        p_first_name := NULL;
        p_last_name := NULL;
        p_salary := NULL;
END;
/

DECLARE
    v_first_name employees.first_name%TYPE;
    v_last_name  employees.last_name%TYPE;
    v_salary     employees.salary%TYPE;
BEGIN
    -- Call the procedure, passing a value to the IN parameter (which becomes a bind variable)
    get_employee_details(100, v_first_name, v_last_name, v_salary);
    DBMS_OUTPUT.PUT_LINE('Employee 100: ' || v_first_name || ' ' || v_last_name || ', Salary: ' || v_salary);

    get_employee_details(102, v_first_name, v_last_name, v_salary);
    DBMS_OUTPUT.PUT_LINE('Employee 102: ' || v_first_name || ' ' || v_last_name || ', Salary: ' || v_salary);

    get_employee_details(999, v_first_name, v_last_name, v_salary); -- Non-existent employee
    IF v_first_name IS NULL THEN
        DBMS_OUTPUT.PUT_LINE('Employee 999 not found.');
    END IF;
END;
/

Explanation This example demonstrates how IN parameters in PL/SQL procedures and functions naturally act as bind variables when used in the SQL statements within those subprograms. This is the most common way to leverage bind variables in modular PL/SQL development.

 

Example 4: Using FORALL with Bind Variables for Bulk DML (Advanced)

DECLARE
    -- Define a nested table type for employee IDs and new salaries
    TYPE employee_id_list IS TABLE OF employees.employee_id%TYPE;
    TYPE new_salary_list IS TABLE OF employees.salary%TYPE;

    -- Declare variables of the defined types
    l_employee_ids employee_id_list := employee_id_list(100, 101, 102); -- Existing employee IDs
    l_new_salaries new_salary_list := new_salary_list(7000, 8500, 4800); -- New salaries
BEGIN
    -- Bulk update using FORALL and bind variables (collection elements are bound)
    -- This significantly improves performance for DML operations on collections.
    FORALL i IN 1 .. l_employee_ids.COUNT
        UPDATE employees
        SET salary = l_new_salaries(i) -- Bind variable from collection
        WHERE employee_id = l_employee_ids(i); -- Bind variable from collection

    DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT || ' rows updated in bulk.');

    -- Rollback the transaction to keep the table clean
    ROLLBACK;
    DBMS_OUTPUT.PUT_LINE('Transaction rolled back.');
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error during bulk update: ' || SQLERRM);
        ROLLBACK;
END;
/

Explanation This advanced example showcases FORALL, a powerful construct for bulk DML operations. When FORALL is used, each element of the PL/SQL collection (e.g., l_new_salaries(i)) acts as a bind variable to the SQL statement, leading to massive performance gains by reducing context switching between the PL/SQL and SQL engines.

 

Example 5: Retrieving Multiple Rows with BULK COLLECT and Bind Variables (Advanced)

DECLARE
    -- Define record type for employee details
    TYPE emp_details_rec IS RECORD (
        employee_id employees.employee_id%TYPE,
        first_name  employees.first_name%TYPE,
        last_name   employees.last_name%TYPE,
        salary      employees.salary%TYPE
    );

    -- Define a nested table type of the record
    TYPE emp_details_tab IS TABLE OF emp_details_rec;

    -- Declare a variable of the nested table type to hold fetched rows
    l_emp_data emp_details_tab;

    -- Bind variable for the salary threshold
    p_salary_threshold NUMBER := 6000;
BEGIN
    -- Fetch multiple rows into the collection using BULK COLLECT
    -- The :p_salary_threshold is a bind variable.
    SELECT employee_id, first_name, last_name, salary
    BULK COLLECT INTO l_emp_data
    FROM employees
    WHERE salary > :p_salary_threshold
    ORDER BY salary DESC;

    DBMS_OUTPUT.PUT_LINE('Employees with salary > ' || p_salary_threshold || ':');
    DBMS_OUTPUT.PUT_LINE('------------------------------------');

    IF l_emp_data.COUNT > 0 THEN
        FOR i IN 1 .. l_emp_data.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('  ID: ' || l_emp_data(i).employee_id ||
                                 ', Name: ' || l_emp_data(i).first_name || ' ' || l_emp_data(i).last_name ||
                                 ', Salary: ' || l_emp_data(i).salary);
        END LOOP;
    ELSE
        DBMS_OUTPUT.PUT_LINE('No employees found above the threshold.');
    END IF;

    -- Change the threshold and re-execute
    p_salary_threshold := 10000;
    SELECT employee_id, first_name, last_name, salary
    BULK COLLECT INTO l_emp_data
    FROM employees
    WHERE salary > :p_salary_threshold
    ORDER BY salary DESC;

    DBMS_OUTPUT.PUT_LINE('Employees with salary > ' || p_salary_threshold || ' (re-run):');
    DBMS_OUTPUT.PUT_LINE('------------------------------------');
    IF l_emp_data.COUNT > 0 THEN
        FOR i IN 1 .. l_emp_data.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('  ID: ' || l_emp_data(i).employee_id ||
                                 ', Name: ' || l_emp_data(i).first_name || ' ' || l_emp_data(i).last_name ||
                                 ', Salary: ' || l_emp_data(i).salary);
        END LOOP;
    ELSE
        DBMS_OUTPUT.PUT_LINE('No employees found above the new threshold.');
    END IF;
END;
/

Explanation This advanced example demonstrates BULK COLLECT for efficiently retrieving multiple rows from the database into a PL/SQL collection. The WHERE clause utilizes a bind variable (:p_salary_threshold), allowing the SQL statement to be reused for different salary criteria while minimizing context switching and enhancing performance. This is a crucial technique for high-performance data retrieval in Oracle PL/SQL.