Dynamic SQL


The EXECUTE IMMEDIATE Statement

The EXECUTE IMMEDIATE statement is the cornerstone of Native Dynamic SQL (NDS) in Oracle PL/SQL. It allows you to execute SQL statements and PL/SQL blocks that are constructed as character strings at runtime. This means you can create highly flexible and generic code that can adapt to different scenarios without being hardcoded. Think of it as telling Oracle, "Here's a piece of SQL I just built, please run it for me now!"

Example 1: Executing a DDL Statement with EXECUTE IMMEDIATE

This example demonstrates how to create a table dynamically using EXECUTE IMMEDIATE. This is often useful in administrative scripts or installation routines where table names might be user-defined.

DECLARE
    v_table_name VARCHAR2(100) := 'EMPLOYEE_AUDIT_' || TO_CHAR(SYSDATE, 'YYYYMMDD');
    v_sql_stmt   VARCHAR2(500);
BEGIN
    -- Construct the DDL statement dynamically
    v_sql_stmt := 'CREATE TABLE ' || v_table_name || ' ( ' ||
                  '  audit_id NUMBER PRIMARY KEY, ' ||
                  '  employee_id NUMBER, ' ||
                  '  change_date DATE, ' ||
                  '  description VARCHAR2(255) ' ||
                  ')';

    -- Execute the DDL statement
    EXECUTE IMMEDIATE v_sql_stmt;

    DBMS_OUTPUT.PUT_LINE('Table ' || v_table_name || ' created successfully.');

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error creating table: ' || SQLERRM);
END;
/

Explanation

In this example, we declare two variables: v_table_name to hold the dynamically generated table name (including a date for uniqueness) and v_sql_stmt to store the complete CREATE TABLE statement. We concatenate strings to build the CREATE TABLE DDL statement. Finally, EXECUTE IMMEDIATE v_sql_stmt; executes the constructed SQL string, creating the new table. This demonstrates the power of EXECUTE IMMEDIATE for dynamic Data Definition Language (DDL) operations.

Example 2: Executing a DML Statement with EXECUTE IMMEDIATE

Here, we'll see how to perform a dynamic INSERT operation. This can be handy when you need to insert data into different tables or columns based on certain conditions.

DECLARE
    v_table_name VARCHAR2(100) := 'AUDIT_LOG';
    v_column1    VARCHAR2(50)  := 'log_id';
    v_column2    VARCHAR2(50)  := 'log_message';
    v_value1     NUMBER        := 1001;
    v_value2     VARCHAR2(200) := 'User login event.';
    v_sql_stmt   VARCHAR2(500);
BEGIN
    -- Assume AUDIT_LOG table already exists with log_id and log_message columns
    -- For demonstration, let's create it if it doesn't exist
    BEGIN
        EXECUTE IMMEDIATE 'CREATE TABLE AUDIT_LOG (log_id NUMBER, log_message VARCHAR2(200))';
    EXCEPTION
        WHEN OTHERS THEN
            NULL; -- Table likely already exists
    END;

    -- Construct the DML (INSERT) statement dynamically
    v_sql_stmt := 'INSERT INTO ' || v_table_name || ' (' || v_column1 || ', ' || v_column2 || ') ' ||
                  'VALUES (:1, :2)'; -- Using bind variables for security and performance

    -- Execute the DML statement with bind variables
    EXECUTE IMMEDIATE v_sql_stmt USING v_value1, v_value2;

    DBMS_OUTPUT.PUT_LINE('Row inserted into ' || v_table_name || ' successfully.');

    -- Optional: Verify the insert
    FOR rec IN (SELECT log_id, log_message FROM AUDIT_LOG WHERE log_id = v_value1) LOOP
        DBMS_OUTPUT.PUT_LINE('Verified: ID=' || rec.log_id || ', Message=' || rec.log_message);
    END LOOP;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error inserting data: ' || SQLERRM);
END;
/

Explanation

This example demonstrates a dynamic INSERT statement. We build the INSERT string, including the table and column names dynamically. Crucially, notice the use of :1 and :2 as placeholders for bind variables. This is a best practice for dynamic SQL as it prevents SQL injection vulnerabilities and improves performance by allowing Oracle to reuse parsed statements. The USING clause (which we'll discuss in more detail shortly) provides the actual values for these placeholders.

Example 3: Executing a Dynamic PL/SQL Block

EXECUTE IMMEDIATE isn't just for SQL statements; you can also execute entire PL/SQL blocks dynamically. This is incredibly powerful for complex scenarios where you need to run custom logic based on runtime conditions.

DECLARE
    v_plsql_block VARCHAR2(1000);
    v_message     VARCHAR2(100) := 'Hello from dynamic PL/SQL!';
    v_result      VARCHAR2(100);
BEGIN
    -- Construct a dynamic PL/SQL block
    -- The PL/SQL block assigns a value to an OUT bind variable
    v_plsql_block := 'BEGIN :result := :message || '' - Processed''; END;';

    -- Execute the dynamic PL/SQL block with IN and OUT bind variables
    EXECUTE IMMEDIATE v_plsql_block USING OUT v_result, IN v_message;

    DBMS_OUTPUT.PUT_LINE('Original message: ' || v_message);
    DBMS_OUTPUT.PUT_LINE('Result from dynamic PL/SQL: ' || v_result);

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error executing dynamic PL/SQL: ' || SQLERRM);
END;
/

Explanation

Here, we construct a PL/SQL anonymous block as a string. This block takes an IN parameter (:message) and an OUT parameter (:result). When EXECUTE IMMEDIATE is called, the USING clause specifies both the OUT and IN variables. The dynamic PL/SQL block executes, modifies the result variable, and that modified value is then available in the v_result variable in the calling block.

Example 4: Dynamic UPDATE Statement

This example shows how to build and execute a dynamic UPDATE statement. This can be useful for generic update routines where the target table, columns, and conditions might vary.

DECLARE
    v_table_name    VARCHAR2(100) := 'EMPLOYEES'; -- Assuming EMPLOYEES table exists
    v_set_column    VARCHAR2(50)  := 'salary';
    v_where_column  VARCHAR2(50)  := 'employee_id';
    v_new_salary    NUMBER        := 65000;
    v_employee_id   NUMBER        := 101;
    v_sql_stmt      VARCHAR2(500);
BEGIN
    -- For demonstration, ensure a row exists
    BEGIN
        EXECUTE IMMEDIATE 'INSERT INTO EMPLOYEES (employee_id, salary) VALUES (101, 60000)';
    EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN
            NULL; -- Employee already exists
    END;

    -- Construct the dynamic UPDATE statement
    v_sql_stmt := 'UPDATE ' || v_table_name ||
                  ' SET ' || v_set_column || ' = :new_value ' ||
                  ' WHERE ' || v_where_column || ' = :where_value';

    -- Execute the UPDATE statement with bind variables
    EXECUTE IMMEDIATE v_sql_stmt USING v_new_salary, v_employee_id;

    DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_id || ' salary updated to ' || v_new_salary || ' successfully.');

    -- Verify the update
    DECLARE
        current_salary NUMBER;
    BEGIN
        EXECUTE IMMEDIATE 'SELECT salary FROM EMPLOYEES WHERE employee_id = :id' INTO current_salary USING v_employee_id;
        DBMS_OUTPUT.PUT_LINE('Verified current salary for ' || v_employee_id || ': ' || current_salary);
    END;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error updating data: ' || SQLERRM);
END;
/

Explanation

This example constructs an UPDATE statement dynamically, allowing the table, column to be updated, and the WHERE clause column to be specified at runtime. Again, bind variables (:new_value, :where_value) are used to pass the new salary and employee ID safely and efficiently. After the update, a dynamic SELECT is used to verify the change, demonstrating how dynamic SQL can be used for both DML and DQL.

Example 5: Dynamic DELETE Statement

Finally, let's look at a dynamic DELETE statement. This is useful for cleanup operations or when you need to remove data based on flexible criteria.

DECLARE
    v_table_name    VARCHAR2(100) := 'TEMP_DATA';
    v_where_column  VARCHAR2(50)  := 'status';
    v_where_value   VARCHAR2(50)  := 'OBSOLETE';
    v_sql_stmt      VARCHAR2(500);
    v_rows_deleted  NUMBER;
BEGIN
    -- For demonstration, create and insert some data
    BEGIN
        EXECUTE IMMEDIATE 'CREATE TABLE TEMP_DATA (id NUMBER, status VARCHAR2(50))';
    EXCEPTION
        WHEN OTHERS THEN
            NULL; -- Table likely already exists
    END;
    EXECUTE IMMEDIATE 'INSERT INTO TEMP_DATA (id, status) VALUES (1, ''ACTIVE'')';
    EXECUTE IMMEDIATE 'INSERT INTO TEMP_DATA (id, status) VALUES (2, ''OBSOLETE'')';
    EXECUTE IMMEDIATE 'INSERT INTO TEMP_DATA (id, status) VALUES (3, ''ACTIVE'')';
    EXECUTE IMMEDIATE 'INSERT INTO TEMP_DATA (id, status) VALUES (4, ''OBSOLETE'')';
    COMMIT;

    DBMS_OUTPUT.PUT_LINE('Initial row count for OBSOLETE status:');
    DECLARE
        initial_count NUMBER;
    BEGIN
        EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM TEMP_DATA WHERE status = :status' INTO initial_count USING v_where_value;
        DBMS_OUTPUT.PUT_LINE('Count: ' || initial_count);
    END;

    -- Construct the dynamic DELETE statement
    v_sql_stmt := 'DELETE FROM ' || v_table_name ||
                  ' WHERE ' || v_where_column || ' = :delete_value';

    -- Execute the DELETE statement with a bind variable
    EXECUTE IMMEDIATE v_sql_stmt USING v_where_value;

    -- Get the number of rows deleted using SQL%ROWCOUNT
    v_rows_deleted := SQL%ROWCOUNT;

    DBMS_OUTPUT.PUT_LINE(v_rows_deleted || ' rows deleted from ' || v_table_name || ' where ' || v_where_column || ' = ''' || v_where_value || ''' successfully.');

    -- Verify the deletion
    DBMS_OUTPUT.PUT_LINE('Final row count for OBSOLETE status:');
    DECLARE
        final_count NUMBER;
    BEGIN
        EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM TEMP_DATA WHERE status = :status' INTO final_count USING v_where_value;
        DBMS_OUTPUT.PUT_LINE('Count: ' || final_count);
    END;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error deleting data: ' || SQLERRM);
END;
/

Explanation

This example demonstrates a dynamic DELETE operation. The table name, the column to filter on, and the value for the filter are all provided dynamically. The SQL%ROWCOUNT attribute is used after EXECUTE IMMEDIATE to determine how many rows were affected by the DELETE statement, which is a common practice in DML operations. We also include checks for initial and final row counts to clearly illustrate the effect of the dynamic delete.

 

The USING Clause for Bind Variables

The USING clause is an integral part of EXECUTE IMMEDIATE when your dynamic SQL statement contains bind variables. Bind variables are placeholders (like :1, :name, etc.) in your SQL string that you later associate with actual PL/SQL variables or expressions. The USING clause provides a secure and efficient way to pass data into your dynamic SQL statements. This is crucial for preventing SQL injection attacks and improving performance by allowing Oracle to parse and cache SQL statements more effectively.

Example 1: Single IN Bind Variable

This example shows how to pass a single input value into a dynamic SELECT statement using the USING clause.

DECLARE
    v_employee_id NUMBER := 100;
    v_employee_name VARCHAR2(100);
    v_sql_stmt    VARCHAR2(200);
BEGIN
    -- Assume EMPLOYEES table exists with employee_id and employee_name
    -- For demonstration, insert a sample record if not present
    BEGIN
        EXECUTE IMMEDIATE 'INSERT INTO EMPLOYEES (employee_id, employee_name) VALUES (100, ''John Doe'')';
    EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN NULL;
    END;

    -- Construct the dynamic SELECT statement with a bind variable
    v_sql_stmt := 'SELECT employee_name FROM EMPLOYEES WHERE employee_id = :emp_id';

    -- Execute the dynamic SELECT using the INTO clause for the result
    -- and the USING clause for the bind variable.
    EXECUTE IMMEDIATE v_sql_stmt INTO v_employee_name USING v_employee_id;

    DBMS_OUTPUT.PUT_LINE('Employee Name for ID ' || v_employee_id || ': ' || v_employee_name);

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('No employee found with ID: ' || v_employee_id);
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/

Explanation

Here, v_sql_stmt contains a SELECT statement with a placeholder :emp_id. The USING v_employee_id clause in the EXECUTE IMMEDIATE statement tells Oracle to substitute the value of v_employee_id for :emp_id before executing the query. This is a fundamental way to pass input parameters to dynamic SQL.

Example 2: Multiple IN Bind Variables

You can pass multiple input values to your dynamic SQL by listing them in the USING clause, separated by commas. The order of variables in the USING clause corresponds to the order of placeholders in your SQL string (if using positional bind variables like :1, :2).

DECLARE
    v_first_name  VARCHAR2(50)  := 'Jane';
    v_last_name   VARCHAR2(50)  := 'Smith';
    v_email       VARCHAR2(100) := 'jane.smith@example.com';
    v_sql_stmt    VARCHAR2(500);
BEGIN
    -- Assume EMPLOYEES table exists with first_name, last_name, email
    -- For demonstration, create the table if it doesn't exist
    BEGIN
        EXECUTE IMMEDIATE 'CREATE TABLE EMPLOYEES (first_name VARCHAR2(50), last_name VARCHAR2(50), email VARCHAR2(100))';
    EXCEPTION
        WHEN OTHERS THEN NULL;
    END;

    -- Construct a dynamic INSERT statement with multiple bind variables
    v_sql_stmt := 'INSERT INTO EMPLOYEES (first_name, last_name, email) ' ||
                  'VALUES (:fn, :ln, :em)';

    -- Execute the INSERT statement using the USING clause for all input values
    EXECUTE IMMEDIATE v_sql_stmt USING v_first_name, v_last_name, v_email;

    DBMS_OUTPUT.PUT_LINE('New employee ' || v_first_name || ' ' || v_last_name || ' inserted.');

    -- Verify the insert
    DECLARE
        v_check_email VARCHAR2(100);
    BEGIN
        EXECUTE IMMEDIATE 'SELECT email FROM EMPLOYEES WHERE first_name = :fn AND last_name = :ln'
        INTO v_check_email USING v_first_name, v_last_name;
        DBMS_OUTPUT.PUT_LINE('Verified email for ' || v_first_name || ' ' || v_last_name || ': ' || v_check_email);
    END;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error inserting employee: ' || SQLERRM);
END;
/

Explanation

In this example, we're performing a dynamic INSERT into the EMPLOYEES table. The v_sql_stmt uses named bind variables (:fn, :ln, :em). The USING clause then maps the PL/SQL variables v_first_name, v_last_name, and v_email to these bind variables in the order they appear in the USING clause.

Example 3: IN OUT Bind Variable

The USING clause also supports IN OUT bind variables, which are variables that are passed into the dynamic SQL and then potentially modified and passed back out.

DECLARE
    v_counter    NUMBER := 5;
    v_sql_stmt   VARCHAR2(200);
BEGIN
    -- Construct a dynamic PL/SQL block that increments a counter
    v_sql_stmt := 'BEGIN :counter := :counter + 10; END;';

    -- Execute the dynamic PL/SQL block with an IN OUT bind variable
    EXECUTE IMMEDIATE v_sql_stmt USING IN OUT v_counter;

    DBMS_OUTPUT.PUT_LINE('Counter after dynamic PL/SQL: ' || v_counter); -- Expected: 15

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error with IN OUT bind variable: ' || SQLERRM);
END;
/

Explanation

Here, v_counter is initially 5. The dynamic PL/SQL block increments v_counter by 10. By specifying IN OUT v_counter in the USING clause, the initial value of v_counter is passed into the dynamic block, and the modified value from the dynamic block is passed back out to the v_counter variable in the calling scope.

Example 4: OUT Bind Variable for Function Return Value

You can use an OUT bind variable to capture a return value from a dynamic PL/SQL function call or a SELECT statement within a PL/SQL block.

DECLARE
    v_current_date DATE;
    v_sql_stmt     VARCHAR2(200);
BEGIN
    -- Construct a dynamic PL/SQL block that returns the current date
    v_sql_stmt := 'BEGIN :out_date := SYSDATE; END;';

    -- Execute the dynamic PL/SQL block, capturing the output into v_current_date
    EXECUTE IMMEDIATE v_sql_stmt USING OUT v_current_date;

    DBMS_OUTPUT.PUT_LINE('Current Date from dynamic PL/SQL: ' || TO_CHAR(v_current_date, 'YYYY-MM-DD HH24:MI:SS'));

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error with OUT bind variable: ' || SQLERRM);
END;
/

Explanation

This example demonstrates how to use an OUT bind variable to retrieve a value from a dynamic PL/SQL block. The dynamic block simply assigns SYSDATE to the :out_date bind variable. The USING OUT v_current_date clause ensures that the value assigned to :out_date inside the dynamic block is returned and stored in v_current_date.

Example 5: Mixed IN and OUT Bind Variables

It's common to have a mix of IN and OUT bind variables within the same EXECUTE IMMEDIATE call.

DECLARE
    v_input_number  NUMBER := 10;
    v_output_square NUMBER;
    v_sql_stmt      VARCHAR2(200);
BEGIN
    -- Construct a dynamic PL/SQL block that squares an input number
    v_sql_stmt := 'BEGIN :output_val := :input_val * :input_val; END;';

    -- Execute the dynamic PL/SQL block with an IN and an OUT bind variable
    EXECUTE IMMEDIATE v_sql_stmt USING OUT v_output_square, IN v_input_number;

    DBMS_OUTPUT.PUT_LINE('Input Number: ' || v_input_number);
    DBMS_OUTPUT.PUT_LINE('Output Square: ' || v_output_square);

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error with mixed IN/OUT bind variables: ' || SQLERRM);
END;
/

Explanation

Here, we have an IN variable v_input_number and an OUT variable v_output_square. The dynamic PL/SQL block performs a calculation using the IN variable and assigns the result to the OUT variable. The order in the USING clause matters: OUT variables are typically listed before IN variables if both are present for clarity, but the actual order of declaration in the dynamic statement (e.g., :output_val := :input_val * :input_val;) determines the flow. However, Oracle matches bind variables by their names or by their positional order if using numbers (e.g., :1, :2). It is important to match the types and directions correctly.

 

The INTO Clause for Single Rows

The INTO clause in EXECUTE IMMEDIATE is used when your dynamic SQL statement is a SELECT query that is expected to return exactly one row. If the query returns zero rows, a NO_DATA_FOUND exception is raised. If it returns more than one row, a TOO_MANY_ROWS exception is raised. It's essential for handling single-row queries dynamically.

Example 1: Retrieving a Single Column

This example demonstrates fetching a single column value from a dynamic SELECT statement.

DECLARE
    v_employee_id NUMBER := 102;
    v_employee_email VARCHAR2(100);
    v_sql_stmt    VARCHAR2(200);
BEGIN
    -- Assume EMPLOYEES table with employee_id and email
    -- For demonstration, insert a sample record
    BEGIN
        EXECUTE IMMEDIATE 'INSERT INTO EMPLOYEES (employee_id, employee_name, email) VALUES (102, ''Alice'', ''alice@example.com'')';
    EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN NULL;
    END;

    -- Construct the dynamic SELECT statement
    v_sql_stmt := 'SELECT email FROM EMPLOYEES WHERE employee_id = :id';

    -- Execute the dynamic SELECT using INTO to capture the result
    EXECUTE IMMEDIATE v_sql_stmt INTO v_employee_email USING v_employee_id;

    DBMS_OUTPUT.PUT_LINE('Email for Employee ID ' || v_employee_id || ': ' || v_employee_email);

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('No employee found with ID: ' || v_employee_id);
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('More than one employee found with ID: ' || v_employee_id || '. This should not happen for a unique ID.');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/

Explanation

Here, the SELECT statement is constructed to retrieve the email for a specific employee_id. The INTO v_employee_email clause directs the single fetched value into the v_employee_email PL/SQL variable. The USING clause provides the value for the bind variable.

Example 2: Retrieving Multiple Columns

You can fetch multiple columns into multiple PL/SQL variables using the INTO clause. The number and data types of the variables in the INTO clause must match the number and data types of the columns in the SELECT list.

DECLARE
    v_product_id    NUMBER := 1;
    v_product_name  VARCHAR2(100);
    v_price         NUMBER;
    v_sql_stmt      VARCHAR2(300);
BEGIN
    -- Assume PRODUCTS table with product_id, product_name, price
    -- For demonstration, insert a sample record
    BEGIN
        EXECUTE IMMEDIATE 'CREATE TABLE PRODUCTS (product_id NUMBER PRIMARY KEY, product_name VARCHAR2(100), price NUMBER)';
    EXCEPTION
        WHEN OTHERS THEN NULL;
    END;
    BEGIN
        EXECUTE IMMEDIATE 'INSERT INTO PRODUCTS (product_id, product_name, price) VALUES (1, ''Laptop'', 1200.00)';
    EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN NULL;
    END;

    -- Construct the dynamic SELECT statement for multiple columns
    v_sql_stmt := 'SELECT product_name, price FROM PRODUCTS WHERE product_id = :id';

    -- Execute the dynamic SELECT using INTO for multiple results
    EXECUTE IMMEDIATE v_sql_stmt INTO v_product_name, v_price USING v_product_id;

    DBMS_OUTPUT.PUT_LINE('Product Name: ' || v_product_name || ', Price: ' || v_price);

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('No product found with ID: ' || v_product_id);
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('More than one product found with ID: ' || v_product_id || '. This should not happen for a unique ID.');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/

Explanation

Here, the SELECT statement retrieves both product_name and price. The INTO v_product_name, v_price clause ensures that the values are assigned to the respective variables in the same order as they appear in the SELECT list.

Example 3: Retrieving into a Record Type

For convenience and readability, especially when dealing with many columns, you can retrieve a single row into a user-defined record type or a %ROWTYPE variable.

DECLARE
    TYPE ProductRec IS RECORD (
        product_name VARCHAR2(100),
        price        NUMBER
    );
    v_product_info ProductRec;
    v_product_id   NUMBER := 2;
    v_sql_stmt     VARCHAR2(300);
BEGIN
    -- Assume PRODUCTS table exists from previous example
    -- Insert another sample record
    BEGIN
        EXECUTE IMMEDIATE 'INSERT INTO PRODUCTS (product_id, product_name, price) VALUES (2, ''Mouse'', 25.50)';
    EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN NULL;
    END;

    -- Construct the dynamic SELECT statement
    v_sql_stmt := 'SELECT product_name, price FROM PRODUCTS WHERE product_id = :id';

    -- Execute the dynamic SELECT into a record type
    EXECUTE IMMEDIATE v_sql_stmt INTO v_product_info USING v_product_id;

    DBMS_OUTPUT.PUT_LINE('Product Name (Record): ' || v_product_info.product_name || ', Price (Record): ' || v_product_info.price);

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('No product found with ID: ' || v_product_id);
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('More than one product found with ID: ' || v_product_id || '. This should not happen for a unique ID.');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/

Explanation

This example demonstrates fetching a single row into a ProductRec record type. The INTO v_product_info clause automatically maps the selected columns to the corresponding fields in the record. This makes the code cleaner and easier to manage, especially with wider tables.

Example 4: Handling NO_DATA_FOUND and TOO_MANY_ROWS

It's crucial to handle the exceptions that EXECUTE IMMEDIATE ... INTO can raise.

DECLARE
    v_user_id   NUMBER := 999; -- An ID that likely won't exist
    v_user_name VARCHAR2(100);
    v_sql_stmt  VARCHAR2(200);
BEGIN
    -- Assume USERS table with user_id and user_name
    -- For demonstration, create and populate if not exists
    BEGIN
        EXECUTE IMMEDIATE 'CREATE TABLE USERS (user_id NUMBER PRIMARY KEY, user_name VARCHAR2(100))';
    EXCEPTION
        WHEN OTHERS THEN NULL;
    END;
    -- Insert a duplicate user_id to trigger TOO_MANY_ROWS later
    BEGIN
        EXECUTE IMMEDIATE 'INSERT INTO USERS (user_id, user_name) VALUES (1, ''Admin'')';
        EXECUTE IMMEDIATE 'INSERT INTO USERS (user_id, user_name) VALUES (1, ''SuperUser'')'; -- This will cause DUP_VAL_ON_INDEX for primary key, but for demonstration, assume no unique constraint for a moment. For a better demo, let's change search criteria
        EXECUTE IMMEDIATE 'INSERT INTO USERS (user_id, user_name) VALUES (10, ''UserA'')';
        EXECUTE IMMEDIATE 'INSERT INTO USERS (user_id, user_name) VALUES (20, ''UserB'')';
        EXECUTE IMMEDIATE 'INSERT INTO USERS (user_id, user_name) VALUES (20, ''UserC'')'; -- This will cause TOO_MANY_ROWS if searching by user_id = 20
    EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN NULL;
    END;
    COMMIT;


    DBMS_OUTPUT.PUT_LINE('--- Testing NO_DATA_FOUND ---');
    v_sql_stmt := 'SELECT user_name FROM USERS WHERE user_id = :id';
    BEGIN
        EXECUTE IMMEDIATE v_sql_stmt INTO v_user_name USING v_user_id;
        DBMS_OUTPUT.PUT_LINE('User Name for ID ' || v_user_id || ': ' || v_user_name);
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            DBMS_OUTPUT.PUT_LINE('EXPECTED: No user found with ID: ' || v_user_id);
    END;

    DBMS_OUTPUT.PUT_LINE('--- Testing TOO_MANY_ROWS ---');
    v_user_id := 20; -- This ID has multiple entries in our setup
    BEGIN
        EXECUTE IMMEDIATE v_sql_stmt INTO v_user_name USING v_user_id;
        DBMS_OUTPUT.PUT_LINE('User Name for ID ' || v_user_id || ': ' || v_user_name);
    EXCEPTION
        WHEN TOO_MANY_ROWS THEN
            DBMS_OUTPUT.PUT_LINE('EXPECTED: More than one user found with ID: ' || v_user_id);
    END;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('General Error: ' || SQLERRM);
END;
/

Explanation

This example explicitly demonstrates how to catch and handle NO_DATA_FOUND when the dynamic query returns no rows, and TOO_MANY_ROWS when it returns more than one. These are common scenarios with SELECT INTO, and proper error handling is vital for robust applications.

Example 5: Dynamic COUNT(*) with INTO

While not strictly retrieving a single row of data, COUNT(*) always returns a single value, making it a perfect candidate for EXECUTE IMMEDIATE ... INTO.

DECLARE
    v_table_name VARCHAR2(100) := 'EMPLOYEES';
    v_row_count  NUMBER;
    v_sql_stmt   VARCHAR2(200);
BEGIN
    -- Construct the dynamic COUNT statement
    v_sql_stmt := 'SELECT COUNT(*) FROM ' || v_table_name;

    -- Execute the dynamic COUNT using INTO
    EXECUTE IMMEDIATE v_sql_stmt INTO v_row_count;

    DBMS_OUTPUT.PUT_LINE('Total rows in ' || v_table_name || ': ' || v_row_count);

    -- Dynamic COUNT with WHERE clause
    v_sql_stmt := 'SELECT COUNT(*) FROM ' || v_table_name || ' WHERE salary > :min_salary';
    DECLARE
        v_min_salary NUMBER := 60000;
        v_filtered_count NUMBER;
    BEGIN
        EXECUTE IMMEDIATE v_sql_stmt INTO v_filtered_count USING v_min_salary;
        DBMS_OUTPUT.PUT_LINE('Rows in ' || v_table_name || ' with salary > ' || v_min_salary || ': ' || v_filtered_count);
    END;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error counting rows: ' || SQLERRM);
END;
/

Explanation

This example shows how to dynamically count rows in a table. The v_sql_stmt is built to perform a COUNT(*) query. The INTO v_row_count then captures the single numerical result. This pattern is very common for dynamic reporting or data validation. We also show how to add a dynamic WHERE clause with a bind variable for more specific counts.

 

The BULK COLLECT INTO Clause for Multiple Rows

When your dynamic SQL statement is a SELECT query that is expected to return multiple rows, you should use the BULK COLLECT INTO clause. This technique significantly improves performance by fetching multiple rows at once (in batches) instead of row-by-row processing, which reduces the number of context switches between the SQL and PL/SQL engines. It's ideal for populating collections (nested tables or varrays).

Example 1: Fetching into a Simple Collection of Scalars

This example demonstrates fetching a list of names into a collection of VARCHAR2.

DECLARE
    TYPE NameArray IS TABLE OF VARCHAR2(100);
    v_employee_names NameArray;
    v_department_id  NUMBER := 10;
    v_sql_stmt       VARCHAR2(300);
BEGIN
    -- Assume EMPLOYEES table with employee_name and department_id
    -- For demonstration, insert sample records
    BEGIN
        EXECUTE IMMEDIATE 'CREATE TABLE EMPLOYEES (employee_id NUMBER PRIMARY KEY, employee_name VARCHAR2(100), department_id NUMBER)';
    EXCEPTION
        WHEN OTHERS THEN NULL;
    END;
    BEGIN
        EXECUTE IMMEDIATE 'INSERT INTO EMPLOYEES (employee_id, employee_name, department_id) VALUES (1, ''Alice'', 10)';
        EXECUTE IMMEDIATE 'INSERT INTO EMPLOYEES (employee_id, employee_name, department_id) VALUES (2, ''Bob'', 20)';
        EXECUTE IMMEDIATE 'INSERT INTO EMPLOYEES (employee_id, employee_name, department_id) VALUES (3, ''Charlie'', 10)';
        EXECUTE IMMEDIATE 'INSERT INTO EMPLOYEES (employee_id, employee_name, department_id) VALUES (4, ''David'', 30)';
    EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN NULL;
    END;
    COMMIT;

    -- Construct the dynamic SELECT statement with BULK COLLECT
    v_sql_stmt := 'SELECT employee_name FROM EMPLOYEES WHERE department_id = :dept_id ORDER BY employee_name';

    -- Execute the dynamic SELECT using BULK COLLECT INTO
    EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_employee_names USING v_department_id;

    DBMS_OUTPUT.PUT_LINE('Employees in Department ' || v_department_id || ':');
    IF v_employee_names.COUNT > 0 THEN
        FOR i IN 1..v_employee_names.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('- ' || v_employee_names(i));
        END LOOP;
    ELSE
        DBMS_OUTPUT.PUT_LINE('No employees found in department ' || v_department_id);
    END IF;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error fetching employee names: ' || SQLERRM);
END;
/

Explanation

First, we define a nested table type NameArray. The v_sql_stmt is a standard SELECT query. The magic happens with BULK COLLECT INTO v_employee_names. This tells Oracle to fetch all rows returned by the dynamic query and populate them directly into the v_employee_names collection. The USING clause passes the department ID as a bind variable.

Example 2: Fetching into a Collection of Records

When you need to fetch multiple columns for multiple rows, using a collection of record types is highly efficient and organized.

DECLARE
    TYPE EmployeeInfoRec IS RECORD (
        employee_name VARCHAR2(100),
        department_id NUMBER
    );
    TYPE EmployeeInfoArray IS TABLE OF EmployeeInfoRec;
    v_employee_list EmployeeInfoArray;
    v_min_salary    NUMBER := 10000;
    v_sql_stmt      VARCHAR2(300);
BEGIN
    -- Assume EMPLOYEES table (from previous example) with salary column
    BEGIN
        EXECUTE IMMEDIATE 'ALTER TABLE EMPLOYEES ADD salary NUMBER';
    EXCEPTION
        WHEN OTHERS THEN NULL; -- Column likely exists
    END;
    EXECUTE IMMEDIATE 'UPDATE EMPLOYEES SET salary = 50000 WHERE employee_id = 1';
    EXECUTE IMMEDIATE 'UPDATE EMPLOYEES SET salary = 70000 WHERE employee_id = 2';
    EXECUTE IMMEDIATE 'UPDATE EMPLOYEES SET salary = 60000 WHERE employee_id = 3';
    EXECUTE IMMEDIATE 'UPDATE EMPLOYEES SET salary = 40000 WHERE employee_id = 4';
    COMMIT;

    -- Construct the dynamic SELECT statement for multiple columns into a record
    v_sql_stmt := 'SELECT employee_name, department_id FROM EMPLOYEES WHERE salary > :sal ORDER BY employee_name';

    -- Execute the dynamic SELECT using BULK COLLECT INTO a collection of records
    EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_employee_list USING v_min_salary;

    DBMS_OUTPUT.PUT_LINE('Employees with salary > ' || v_min_salary || ':');
    IF v_employee_list.COUNT > 0 THEN
        FOR i IN 1..v_employee_list.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('- Name: ' || v_employee_list(i).employee_name || ', Dept ID: ' || v_employee_list(i).department_id);
        END LOOP;
    ELSE
        DBMS_OUTPUT.PUT_LINE('No employees found with salary > ' || v_min_salary);
    END IF;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error fetching employee info: ' || SQLERRM);
END;
/

Explanation

Here, we define a record type EmployeeInfoRec and a collection of that record type EmployeeInfoArray. The dynamic SELECT fetches employee_name and department_id. BULK COLLECT INTO v_employee_list populates the collection, with each element being a record holding the corresponding name and department ID. This is a very clean way to handle multi-column, multi-row results.

Example 3: Dynamic WHERE Clause with BULK COLLECT

This example shows how to build a flexible WHERE clause dynamically and still use BULK COLLECT.

DECLARE
    TYPE ProductNames IS TABLE OF VARCHAR2(100);
    v_product_list ProductNames;
    v_category     VARCHAR2(50) := 'Electronics';
    v_min_price    NUMBER       := 500;
    v_sql_stmt     VARCHAR2(500);
    v_where_clause VARCHAR2(200);
BEGIN
    -- Assume PRODUCTS table with category and price
    BEGIN
        EXECUTE IMMEDIATE 'ALTER TABLE PRODUCTS ADD category VARCHAR2(50)';
    EXCEPTION
        WHEN OTHERS THEN NULL;
    END;
    EXECUTE IMMEDIATE 'UPDATE PRODUCTS SET category = ''Electronics'' WHERE product_id = 1';
    EXECUTE IMMEDIATE 'UPDATE PRODUCTS SET category = ''Office'' WHERE product_id = 2';
    EXECUTE IMMEDIATE 'INSERT INTO PRODUCTS (product_id, product_name, price, category) VALUES (3, ''Monitor'', 300.00, ''Electronics'')';
    INSERT INTO PRODUCTS (product_id, product_name, price, category) VALUES (4, 'Keyboard', 75.00, 'Electronics');
    INSERT INTO PRODUCTS (product_id, product_name, price, category) VALUES (5, 'Desk', 200.00, 'Office');
    COMMIT;

    -- Construct the dynamic WHERE clause based on conditions
    v_where_clause := 'WHERE 1 = 1'; -- Start with a true condition to easily append AND clauses
    IF v_category IS NOT NULL THEN
        v_where_clause := v_where_clause || ' AND category = :cat';
    END IF;
    IF v_min_price IS NOT NULL THEN
        v_where_clause := v_where_clause || ' AND price >= :price';
    END IF;

    -- Construct the full dynamic SQL statement
    v_sql_stmt := 'SELECT product_name FROM PRODUCTS ' || v_where_clause || ' ORDER BY product_name';

    -- Execute the dynamic SELECT using BULK COLLECT and conditionally add USING clause variables
    IF v_category IS NOT NULL AND v_min_price IS NOT NULL THEN
        EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_product_list USING v_category, v_min_price;
    ELSIF v_category IS NOT NULL THEN
        EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_product_list USING v_category;
    ELSIF v_min_price IS NOT NULL THEN
        EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_product_list USING v_min_price;
    ELSE
        EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_product_list;
    END IF;

    DBMS_OUTPUT.PUT_LINE('Products matching criteria:');
    IF v_product_list.COUNT > 0 THEN
        FOR i IN 1..v_product_list.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('- ' || v_product_list(i));
        END LOOP;
    ELSE
        DBMS_OUTPUT.PUT_LINE('No products found matching the criteria.');
    END IF;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error fetching products with dynamic WHERE: ' || SQLERRM);
END;
/
/

Explanation

This is a more advanced example where the WHERE clause itself is built dynamically based on whether v_category and v_min_price have values. The USING clause is then conditionally constructed based on which bind variables are actually needed. This demonstrates how flexible dynamic SQL can be when combined with conditional logic.

Example 4: Handling Empty Result Sets with BULK COLLECT

Unlike INTO, BULK COLLECT INTO does not raise a NO_DATA_FOUND exception if the query returns zero rows. The collection will simply be empty.

DECLARE
    TYPE EmployeeIDs IS TABLE OF NUMBER;
    v_employee_ids EmployeeIDs;
    v_non_existent_dept_id NUMBER := 99;
    v_sql_stmt             VARCHAR2(200);
BEGIN
    -- Construct the dynamic SELECT statement
    v_sql_stmt := 'SELECT employee_id FROM EMPLOYEES WHERE department_id = :dept_id';

    -- Execute the dynamic SELECT for a non-existent department
    EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_employee_ids USING v_non_existent_dept_id;

    DBMS_OUTPUT.PUT_LINE('Employees in Department ' || v_non_existent_dept_id || ':');
    IF v_employee_ids.COUNT > 0 THEN
        FOR i IN 1..v_employee_ids.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('- Employee ID: ' || v_employee_ids(i));
        END LOOP;
    ELSE
        DBMS_OUTPUT.PUT_LINE('No employees found in department ' || v_non_existent_dept_id || '. (Expected behavior, no NO_DATA_FOUND)');
    END IF;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/

Explanation

This example highlights a key difference: BULK COLLECT INTO gracefully handles queries that return no rows by simply returning an empty collection. You should check collection.COUNT > 0 to determine if any rows were fetched. This is safer than INTO for scenarios where zero rows are a valid outcome.

Example 5: Dynamic ORDER BY and BULK COLLECT

You can dynamically change the ORDER BY clause of your SELECT statement, making your reports or data displays highly customizable.

DECLARE
    TYPE EmployeeNames IS TABLE OF VARCHAR2(100);
    v_employee_names EmployeeNames;
    v_order_by_column VARCHAR2(50) := 'employee_name'; -- Can be 'employee_id' or 'salary'
    v_order_direction VARCHAR2(10) := 'ASC';             -- Can be 'DESC'
    v_sql_stmt        VARCHAR2(300);
BEGIN
    -- Ensure all columns are available for order by
    BEGIN
        EXECUTE IMMEDIATE 'ALTER TABLE EMPLOYEES ADD (employee_id NUMBER, salary NUMBER)';
    EXCEPTION
        WHEN OTHERS THEN NULL;
    END;
    -- Just to be safe for existing data, update some values
    EXECUTE IMMEDIATE 'UPDATE EMPLOYEES SET employee_id = 1, salary = 50000 WHERE employee_name = ''Alice''';
    EXECUTE IMMEDIATE 'UPDATE EMPLOYEES SET employee_id = 2, salary = 70000 WHERE employee_name = ''Bob''';
    EXECUTE IMMEDIATE 'UPDATE EMPLOYEES SET employee_id = 3, salary = 60000 WHERE employee_name = ''Charlie''';
    EXECUTE IMMEDIATE 'UPDATE EMPLOYEES SET employee_id = 4, salary = 40000 WHERE employee_name = ''David''';
    COMMIT;


    -- Construct the dynamic SELECT statement with dynamic ORDER BY
    v_sql_stmt := 'SELECT employee_name FROM EMPLOYEES ORDER BY ' || v_order_by_column || ' ' || v_order_direction;

    -- Execute the dynamic SELECT using BULK COLLECT
    EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_employee_names;

    DBMS_OUTPUT.PUT_LINE('Employees sorted by ' || v_order_by_column || ' ' || v_order_direction || ':');
    IF v_employee_names.COUNT > 0 THEN
        FOR i IN 1..v_employee_names.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('- ' || v_employee_names(i));
        END LOOP;
    ELSE
        DBMS_OUTPUT.PUT_LINE('No employees found.');
    END IF;

    -- Change order by to salary descending
    v_order_by_column := 'salary';
    v_order_direction := 'DESC';
    v_sql_stmt := 'SELECT employee_name FROM EMPLOYEES ORDER BY ' || v_order_by_column || ' ' || v_order_direction;
    EXECUTE IMMEDIATE v_sql_stmt BULK COLLECT INTO v_employee_names;
    DBMS_OUTPUT.PUT_LINE('Employees sorted by ' || v_order_by_column || ' ' || v_order_direction || ':');
    IF v_employee_names.COUNT > 0 THEN
        FOR i IN 1..v_employee_names.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('- ' || v_employee_names(i));
        END LOOP;
    ELSE
        DBMS_OUTPUT.PUT_LINE('No employees found.');
    END IF;


EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error fetching employees with dynamic ORDER BY: ' || SQLERRM);
END;
/

Explanation

This example demonstrates a powerful use case: allowing users to specify the sorting criteria for a report. The ORDER BY clause is constructed using PL/SQL variables, and then the full query is executed with BULK COLLECT. This provides immense flexibility without requiring a separate, hardcoded query for each sort order.

 

The DBMS_SQL Package

The DBMS_SQL package provides a more powerful and flexible (but also more complex) interface for building and executing dynamic SQL compared to Native Dynamic SQL (EXECUTE IMMEDIATE). It allows you to parse, bind, and execute SQL statements step-by-step, offering fine-grained control, especially for queries where the number or data types of bind variables or select-list items are not known until runtime. It's often referred to as "Method 4" dynamic SQL, while NDS is "Method 3".

 

When to use DBMS_SQL

You should consider using the DBMS_SQL package in the following scenarios:

Unknown Number/Types of Bind Variables: When the number of bind variables or their data types in your SQL statement are not known until runtime. For example, building a search query where users can select an arbitrary number of filter criteria.

Unknown Number/Types of Select-List Items: When the columns you need to retrieve from a SELECT statement are not fixed and can vary at runtime. This is common in generic reporting tools.

Highly Complex Dynamic SQL: For extremely intricate dynamic SQL generation where EXECUTE IMMEDIATE might become unwieldy due to a large number of conditional clauses or variable types.

Backward Compatibility: In older Oracle versions where NDS was not fully featured or available. However, for modern Oracle versions, NDS covers most dynamic SQL needs more simply.

Advanced Features: DBMS_SQL offers some advanced features like describing columns, fetching values by position, and array binding (though NDS also supports array binding for DML).

Example 1: Basic DBMS_SQL for a Fixed Query

While EXECUTE IMMEDIATE is simpler for this, this example illustrates the basic steps of DBMS_SQL for a fixed query.

DECLARE
    v_cursor_id   INTEGER;
    v_rows_parsed NUMBER;
    v_employee_id NUMBER := 101;
    v_employee_name VARCHAR2(100);
    v_sql_stmt    VARCHAR2(200) := 'SELECT employee_name FROM EMPLOYEES WHERE employee_id = :id';
BEGIN
    -- 1. Open a new cursor
    v_cursor_id := DBMS_SQL.OPEN_CURSOR;

    -- 2. Parse the SQL statement
    DBMS_SQL.PARSE(v_cursor_id, v_sql_stmt, DBMS_SQL.NATIVE);

    -- 3. Bind variables (if any)
    DBMS_SQL.BIND_VARIABLE(v_cursor_id, ':id', v_employee_id);

    -- 4. Define output columns
    DBMS_SQL.DEFINE_COLUMN(v_cursor_id, 1, v_employee_name, 100); -- Position 1, variable, max length

    -- 5. Execute the cursor
    v_rows_parsed := DBMS_SQL.EXECUTE(v_cursor_id);

    -- 6. Fetch results (for SELECT statements)
    IF DBMS_SQL.FETCH_ROWS(v_cursor_id) > 0 THEN
        -- 7. Get column values
        DBMS_SQL.COLUMN_VALUE(v_cursor_id, 1, v_employee_name);
        DBMS_OUTPUT.PUT_LINE('Employee Name (DBMS_SQL): ' || v_employee_name);
    ELSE
        DBMS_OUTPUT.PUT_LINE('No employee found with ID: ' || v_employee_id);
    END IF;

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error with DBMS_SQL: ' || SQLERRM);
        IF DBMS_SQL.IS_OPEN(v_cursor_id) THEN
            DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
        END IF;
END;
/

Explanation

This example breaks down the DBMS_SQL process:

OPEN_CURSOR: Obtains a unique cursor ID.

PARSE: Parses the SQL string. DBMS_SQL.NATIVE indicates the native SQL parser.

BIND_VARIABLE: Associates the PL/SQL variable v_employee_id with the bind placeholder :id.

DEFINE_COLUMN: Specifies the data type and maximum length for each column in the select list. This is crucial for DBMS_SQL to know how to retrieve the data.

EXECUTE: Executes the parsed statement. For SELECT statements, it prepares the result set.

FETCH_ROWS: Fetches a row from the result set.

COLUMN_VALUE: Retrieves the value of a specific column from the fetched row into a PL/SQL variable. Finally, CLOSE_CURSOR is essential to release resources. Error handling also closes the cursor.

Example 2: Dynamic DDL with DBMS_SQL

You can also execute DDL statements using DBMS_SQL.

DECLARE
    v_cursor_id INTEGER;
    v_table_name VARCHAR2(100) := 'NEW_LOG_TABLE_' || TO_CHAR(SYSDATE, 'YYYYMMDD_HH24MISS');
    v_sql_stmt   VARCHAR2(500);
BEGIN
    v_cursor_id := DBMS_SQL.OPEN_CURSOR;

    v_sql_stmt := 'CREATE TABLE ' || v_table_name || ' ( ' ||
                  '  log_id NUMBER PRIMARY KEY, ' ||
                  '  log_message VARCHAR2(255), ' ||
                  '  log_date DATE DEFAULT SYSDATE ' ||
                  ')';

    DBMS_SQL.PARSE(v_cursor_id, v_sql_stmt, DBMS_SQL.NATIVE);

    -- For DDL, EXECUTE returns the number of rows processed, which is typically 0
    IF DBMS_SQL.EXECUTE(v_cursor_id) = 0 THEN
        DBMS_OUTPUT.PUT_LINE('Table ' || v_table_name || ' created successfully using DBMS_SQL.');
    ELSE
        DBMS_OUTPUT.PUT_LINE('DDL execution completed with non-zero result. Table ' || v_table_name || ' might have been created.');
    END IF;

    DBMS_SQL.CLOSE_CURSOR(v_cursor_id);

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error creating table with DBMS_SQL: ' || SQLERRM);
        IF DBMS_SQL.IS_OPEN(v_cursor_id) THEN
            DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
        END IF;
END;
/

Explanation

This example is similar to a DDL example with EXECUTE IMMEDIATE, but it uses the explicit DBMS_SQL steps. DBMS_SQL.EXECUTE is called, and for DDL, it typically returns 0 as no rows are directly affected in the same way as DML.

Example 3: Describing Columns for Unknown Select Lists

This is where DBMS_SQL truly shines: when you don't know the columns being selected until runtime.

DECLARE
    v_cursor_id   INTEGER;
    v_sql_stmt    VARCHAR2(500) := 'SELECT product_id, product_name, price FROM PRODUCTS WHERE ROWNUM <= 2';
    v_col_cnt     INTEGER;
    v_rec_tab     DBMS_SQL.DESC_TAB;
    v_name        VARCHAR2(256);
    v_num         NUMBER;
    v_date        DATE;
BEGIN
    v_cursor_id := DBMS_SQL.OPEN_CURSOR;
    DBMS_SQL.PARSE(v_cursor_id, v_sql_stmt, DBMS_SQL.NATIVE);

    -- Describe the columns: get metadata about the select list
    DBMS_SQL.DESCRIBE_COLUMNS(v_cursor_id, v_col_cnt, v_rec_tab);

    -- Define output columns based on description
    FOR i IN 1..v_col_cnt LOOP
        CASE v_rec_tab(i).col_type
            WHEN 1 THEN -- VARCHAR2
                DBMS_SQL.DEFINE_COLUMN(v_cursor_id, i, v_name, 256);
            WHEN 2 THEN -- NUMBER
                DBMS_SQL.DEFINE_COLUMN(v_cursor_id, i, v_num);
            WHEN 12 THEN -- DATE
                DBMS_SQL.DEFINE_COLUMN(v_cursor_id, i, v_date);
            ELSE
                DBMS_SQL.DEFINE_COLUMN(v_cursor_id, i, v_name, 256); -- Default to VARCHAR2 for others
        END CASE;
    END LOOP;

    -- Execute and fetch
    v_num := DBMS_SQL.EXECUTE(v_cursor_id);

    DBMS_OUTPUT.PUT_LINE('Fetched Rows:');
    WHILE DBMS_SQL.FETCH_ROWS(v_cursor_id) > 0 LOOP
        DBMS_OUTPUT.PUT_LINE('--- Row ---');
        FOR i IN 1..v_col_cnt LOOP
            DBMS_OUTPUT.PUT_LINE('Column ' || i || ' Name: ' || v_rec_tab(i).col_name);
            CASE v_rec_tab(i).col_type
                WHEN 1 THEN
                    DBMS_SQL.COLUMN_VALUE(v_cursor_id, i, v_name);
                    DBMS_OUTPUT.PUT_LINE('  Value (VARCHAR2): ' || v_name);
                WHEN 2 THEN
                    DBMS_SQL.COLUMN_VALUE(v_cursor_id, i, v_num);
                    DBMS_OUTPUT.PUT_LINE('  Value (NUMBER): ' || v_num);
                WHEN 12 THEN
                    DBMS_SQL.COLUMN_VALUE(v_cursor_id, i, v_date);
                    DBMS_OUTPUT.PUT_LINE('  Value (DATE): ' || TO_CHAR(v_date, 'YYYY-MM-DD HH24:MI:SS'));
            END CASE;
        END LOOP;
    END LOOP;

    DBMS_SQL.CLOSE_CURSOR(v_cursor_id);

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error describing columns with DBMS_SQL: ' || SQLERRM);
        IF DBMS_SQL.IS_OPEN(v_cursor_id) THEN
            DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
        END IF;
END;
/

Explanation

This is a more complex but powerful DBMS_SQL example. After parsing, DBMS_SQL.DESCRIBE_COLUMNS retrieves metadata about each column in the select list (like name, type, length). We then loop through this metadata to dynamically call DEFINE_COLUMN with the appropriate type. During fetching, we again use the metadata to call COLUMN_VALUE with the correct target variable type. This is essential for building generic query tools where the shape of the result set is not known at compile time.

Example 4: Dynamic Number of Bind Variables with DBMS_SQL

When the number of bind variables can change at runtime, DBMS_SQL provides explicit control.

DECLARE
    v_cursor_id INTEGER;
    v_sql_stmt  VARCHAR2(500);
    v_where_clause VARCHAR2(200);
    v_bind_val_1 VARCHAR2(100);
    v_bind_val_2 NUMBER;
    v_product_name VARCHAR2(100);
BEGIN
    -- Scenario: Build a WHERE clause with optional filters
    v_sql_stmt := 'SELECT product_name FROM PRODUCTS';
    v_where_clause := '';

    -- Conditionally add filters and bind variables
    IF TRUE THEN -- Simulate a condition where category is filtered
        v_where_clause := v_where_clause || ' WHERE category = :cat';
        v_bind_val_1 := 'Electronics';
    END IF;

    IF TRUE THEN -- Simulate a condition where price is filtered
        IF v_where_clause IS NULL THEN
            v_where_clause := ' WHERE price >= :price';
        ELSE
            v_where_clause := v_where_clause || ' AND price >= :price';
        END IF;
        v_bind_val_2 := 100;
    END IF;

    v_sql_stmt := v_sql_stmt || v_where_clause || ' ORDER BY product_name';

    DBMS_OUTPUT.PUT_LINE('Executing SQL: ' || v_sql_stmt);

    v_cursor_id := DBMS_SQL.OPEN_CURSOR;
    DBMS_SQL.PARSE(v_cursor_id, v_sql_stmt, DBMS_SQL.NATIVE);

    -- Bind variables based on conditions
    IF v_bind_val_1 IS NOT NULL THEN
        DBMS_SQL.BIND_VARIABLE(v_cursor_id, ':cat', v_bind_val_1);
    END IF;
    IF v_bind_val_2 IS NOT NULL THEN
        DBMS_SQL.BIND_VARIABLE(v_cursor_id, ':price', v_bind_val_2);
    END IF;

    DBMS_SQL.DEFINE_COLUMN(v_cursor_id, 1, v_product_name, 100);

    IF DBMS_SQL.EXECUTE_AND_FETCH(v_cursor_id, TRUE) > 0 THEN -- Execute and fetch one row
        DBMS_SQL.COLUMN_VALUE(v_cursor_id, 1, v_product_name);
        DBMS_OUTPUT.PUT_LINE('First matching product: ' || v_product_name);
    ELSE
        DBMS_OUTPUT.PUT_LINE('No products found matching criteria.');
    END IF;

    DBMS_SQL.CLOSE_CURSOR(v_cursor_id);

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error with dynamic bind variables in DBMS_SQL: ' || SQLERRM);
        IF DBMS_SQL.IS_OPEN(v_cursor_id) THEN
            DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
        END IF;
END;
/

Explanation

This example builds a WHERE clause dynamically, and critically, it conditionally binds variables based on which parts of the WHERE clause are included. This fine-grained control over binding is a key reason to use DBMS_SQL for truly dynamic queries where the parameters are unknown at compile time.

Example 5: Fetching Multiple Rows with DBMS_SQL (Looping)

While BULK COLLECT with NDS is often preferred, DBMS_SQL can also fetch multiple rows in a loop.

DECLARE
    v_cursor_id   INTEGER;
    v_sql_stmt    VARCHAR2(200) := 'SELECT employee_name, department_id FROM EMPLOYEES ORDER BY employee_name';
    v_employee_name VARCHAR2(100);
    v_department_id NUMBER;
BEGIN
    v_cursor_id := DBMS_SQL.OPEN_CURSOR;
    DBMS_SQL.PARSE(v_cursor_id, v_sql_stmt, DBMS_SQL.NATIVE);

    -- Define output columns
    DBMS_SQL.DEFINE_COLUMN(v_cursor_id, 1, v_employee_name, 100);
    DBMS_SQL.DEFINE_COLUMN(v_cursor_id, 2, v_department_id);

    v_department_id := DBMS_SQL.EXECUTE(v_cursor_id); -- For SELECT, this returns a dummy value, not rows parsed

    DBMS_OUTPUT.PUT_LINE('All Employees:');
    WHILE DBMS_SQL.FETCH_ROWS(v_cursor_id) > 0 LOOP
        DBMS_SQL.COLUMN_VALUE(v_cursor_id, 1, v_employee_name);
        DBMS_SQL.COLUMN_VALUE(v_cursor_id, 2, v_department_id);
        DBMS_OUTPUT.PUT_LINE('- Name: ' || v_employee_name || ', Dept: ' || v_department_id);
    END LOOP;

    DBMS_SQL.CLOSE_CURSOR(v_cursor_id);

EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error fetching multiple rows with DBMS_SQL: ' || SQLERRM);
        IF DBMS_SQL.IS_OPEN(v_cursor_id) THEN
            DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
        END IF;
END;
/

Explanation

This example demonstrates a classic DBMS_SQL loop for fetching multiple rows. After executing the cursor, the WHILE DBMS_SQL.FETCH_ROWS(v_cursor_id) > 0 loop continues to fetch rows one by one. Inside the loop, DBMS_SQL.COLUMN_VALUE retrieves the values for each column into the defined PL/SQL variables.

 

A comparison of NDS and DBMS_SQL

Both Native Dynamic SQL (EXECUTE IMMEDIATE) and the DBMS_SQL package allow you to execute dynamic SQL, but they differ significantly in their approach, complexity, and capabilities. Understanding these differences is key to choosing the right tool for your dynamic SQL needs.

Feature Native Dynamic SQL (EXECUTE IMMEDIATE) DBMS_SQL Package
Simplicity Simpler to use for common dynamic SQL tasks. Fewer lines of code. More complex, requiring explicit cursor management (open, parse, bind, execute, fetch, close).
Performance Generally faster for common use cases. More optimized internally by Oracle. Can be slightly slower due to the overhead of explicit API calls, but offers features that might make it perform better in highly complex scenarios (e.g., describing columns once).
Compile-time vs. Runtime Determined at compile time: The number and data types of bind variables and select list items must be known when writing the PL/SQL code. Determined at runtime: Allows for dynamic discovery of bind variables and select list items, enabling truly generic solutions.
Bind Variables Uses simple positional (:1, :2) or named (:var) bind variables directly in the USING clause. Type checking is done implicitly. Uses explicit BIND_VARIABLE calls. Requires careful matching of variable types and names/positions.
Fetching Rows INTO for single rows (NO_DATA_FOUND, TOO_MANY_ROWS exceptions). BULK COLLECT INTO for multiple rows (no NO_DATA_FOUND). DEFINE_COLUMN for each output column. Loops with FETCH_ROWS and COLUMN_VALUE for row-by-row processing. Can also achieve bulk collection but with more code.
DDL Support Fully supports DDL operations. Fully supports DDL operations.
DML Support Fully supports DML operations. Fully supports DML operations.
PL/SQL Block Support Can execute anonymous PL/SQL blocks. Can execute anonymous PL/SQL blocks.
Security Requires careful use of bind variables to prevent SQL injection. Provides explicit control over binding, which helps prevent SQL injection if used correctly.
Error Handling Raises standard PL/SQL exceptions (e.g., NO_DATA_FOUND, TOO_MANY_ROWS). Raises DBMS_SQL specific exceptions in addition to standard exceptions. Requires explicit cursor closing in exception handlers.
Use Cases Best for dynamic DDL/DML, single-row SELECT INTO, and multi-row BULK COLLECT INTO where the query structure (columns, number of bind vars) is known. Ideal for highly generic query tools, reporting engines, or situations where the SQL statement's structure (number/types of columns or bind variables) is not known until runtime.

 

Key Takeaways:

For 90-95% of dynamic SQL needs, Native Dynamic SQL (EXECUTE IMMEDIATE) is the preferred choice due to its simplicity, readability, and performance.

DBMS_SQL is a powerful, low-level API that provides greater control and flexibility, especially when you need to dynamically discover the structure of a SQL statement (e.g., column names, data types, or the number of bind variables) at runtime.

Always use bind variables regardless of whether you choose NDS or DBMS_SQL to protect against SQL injection and improve performance.

By mastering both EXECUTE IMMEDIATE and DBMS_SQL, you'll be well-equipped to handle any dynamic SQL challenge in your Oracle PL/SQL development.