Dynamic SQL · Guide + 18 Interview Questions

Dynamic SQL: Building Queries at Runtime

Static SQL is known when the code is compiled, while dynamic SQL builds or determines the SQL statement at runtime. This gives you flexibility when parts of a statement depend on runtime information, such as a table name, column name, or a DDL operation that needs to be constructed dynamically. That flexibility also introduces security risks, especially SQL injection, if input is added to a statement without proper validation and handling. This guide explains how dynamic SQL works in Oracle and other database systems, the main ways to execute dynamic SQL, how to use it safely, and the eighteen questions commonly asked in SQL interviews. The examples use the same customer and order tables as the rest of the series, so you can run them against familiar data.

🎯 Key Takeaways
18 Interview Questions Beginner to Advanced Real Examples Updated Oct 7, 2026
Kailas Warade ✓ Verified Author
Data Engineer · Oracle Certified · SQL & PL/SQL
Kailas works with SQL, PL/SQL, Python, and PySpark to build data pipelines. He writes the SQL interview guides at Sankalan Data Tech, focused on practical, real-world answers.
🏆 Oracle Certified 💼 Data Engineer 📝 SQL & PL/SQL Guides 📍 Pune, India
📤 Share:

On This Page

What Dynamic SQL Really Is

Static SQL is written directly in the program and is known when the code is compiled. For example, when the compiler sees SELECT name FROM customers WHERE customer_id = v_id, the database can check the SQL syntax and references to tables and columns as part of compiling the code. This allows many problems to be identified before the program is executed.

Dynamic SQL works differently. The SQL statement is constructed or determined while the program is running, usually as a string, and then passed to the database for parsing and execution. Because the final statement is not known when the surrounding code is compiled, the database cannot validate that complete statement in advance. This is what makes dynamic SQL flexible, but it also means that the code needs additional care.

Dynamic SQL is useful when part of the SQL cannot be known until runtime. For example, a reporting tool may allow users to choose different columns, an administrative script may work with a table name stored in configuration, or a program may need to construct a DDL statement dynamically. It can also be useful when optional filters change the structure of a query. Static SQL is often simpler when the statement is known in advance, so dynamic SQL should be used when the runtime flexibility is actually needed.

Security becomes especially important when dynamic SQL is built from external input. Concatenating user input directly into a SQL string, such as "SELECT * FROM customers WHERE name = '" || user_input || "'", can allow malicious input to change the meaning of the statement. This is known as SQL injection. Using bind variables for values, validating identifiers, and avoiding unnecessary string concatenation are important ways to reduce this risk.

The Sample Data We Use in Every Example

Same customers and orders tables as the rest of the series. Every dynamic SQL example below either queries these tables or generates SQL against them.

-- customers table
-- customer_id | customer_name    | region | credit_limit
-- C001        | Alice Corp       | North  | 50000
-- C002        | Bob Traders      | South  | 25000
-- C003        | Charlie Ltd      | North  | 75000
-- C004        | Diana Retail     | West   | 40000
-- C005        | Ethan Supplies   | South  | 55000
-- C006        | Fiona Mart       | West   | NULL
-- C007        | George Wholesale | North  | 80000

-- orders table
-- order_id | customer_id | order_amount | order_status
-- 1001     | C001        | 12000        | Delivered
-- 1002     | C002        | 4500         | Shipped
-- 1003     | C001        | 7500         | Delivered
-- 1004     | C003        | 22000        | Pending
-- 1005     | C002        | 3200         | Delivered
-- 1006     | C004        | 18000        | Shipped
-- 1007     | C001        | 9000         | Delivered

Two things about this data make the dynamic SQL examples useful:

  • Multiple tables with a foreign key relationship — natural for dynamic join generation.
  • Fiona Mart has a NULL credit limit — useful for showing how dynamic WHERE clauses handle optional filters.

Static SQL vs Dynamic SQL

The distinction sounds theoretical until you hit a problem static SQL cannot solve. Then the trade-offs become concrete.

Aspect Static SQL Dynamic SQL
When parsed At compile time At runtime
Object names Must be known when code is written Can come from variables
Syntax errors caught When the code is compiled Only when the code runs
Execution plan reuse Automatic Only with bind variables
SQL injection risk Very low High if values are concatenated
Readability High Lower — harder to follow
DDL support No Yes, with EXECUTE IMMEDIATE

When static SQL is the right choice

-- Static: the table and columns are known when the code is compiled
SELECT customer_name, credit_limit
FROM   customers
WHERE  customer_id = p_id;

That query names its table and columns. Static SQL parses it once, caches the plan, and reuses everything. This is what you should write whenever possible — it is safer, faster, and easier to debug.

When dynamic SQL is required

-- Dynamic: the table name is not known until runtime
v_sql := 'SELECT COUNT(*) FROM ' || p_table_name;
EXECUTE IMMEDIATE v_sql INTO v_count;

The table name is a variable. Static SQL cannot express that — the compiler does not know what table to validate against. Dynamic SQL is the only tool that can.

The rule that makes decisions easier: if the table names and column names are known when you write the code, use static SQL. Reach for dynamic SQL only when they are not.

EXECUTE IMMEDIATE — The Primary Tool

EXECUTE IMMEDIATE is a single PL/SQL statement that parses and runs one dynamic SQL statement. It is the tool you reach for most of the time, and for most dynamic SQL problems, it is enough.

The basic form

BEGIN
    EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM customers';
END;

The statement parses the SQL text and runs it. If the statement returns no rows — a DDL statement, an INSERT, an UPDATE without a return clause — that is all you need.

Capturing a result with INTO

DECLARE
    v_count NUMBER;
BEGIN
    EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM customers'
        INTO v_count;
    DBMS_OUTPUT.PUT_LINE('Customer count: ' || v_count);
END;

The INTO clause captures the result of the query. The dynamic statement must return exactly one row and one column for this to work, or you get a runtime error.

Passing values with USING

DECLARE
    v_total NUMBER;
BEGIN
    EXECUTE IMMEDIATE
        'SELECT SUM(order_amount) FROM orders WHERE customer_id = :cid'
        INTO v_total
        USING 'C001';
    DBMS_OUTPUT.PUT_LINE('Total for C001: ' || v_total);
END;

The :cid in the SQL text is a bind variable. The USING clause supplies its value. Bind variables are the single most important part of safe dynamic SQL — they keep user input away from the SQL parser.

Returning a value from DML with RETURNING INTO

DECLARE
    v_new_balance NUMBER;
BEGIN
    EXECUTE IMMEDIATE
        'UPDATE accounts SET balance = balance - 500 WHERE account_id = :id RETURNING balance INTO :bal'
        USING 1
        RETURNING INTO v_new_balance;
    DBMS_OUTPUT.PUT_LINE('New balance: ' || v_new_balance);
END;

The RETURNING INTO clause captures a value from the DML statement — a common pattern for balance updates, ID generation, or any write that needs to return a computed value.

Handling errors

BEGIN
    EXECUTE IMMEDIATE 'SELECT * FROM nonexistent_table';
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;

Because the SQL is parsed at runtime, syntax errors and missing tables surface as exceptions. Wrap dynamic SQL in exception handling so a bad string does not crash the calling program.

DBMS_SQL — When the Statement Is Unknown

DBMS_SQL is a procedural package that gives you a cursor you control step by step. Open, parse, bind, execute, fetch, close — every step is a separate procedure call. It is heavier than EXECUTE IMMEDIATE, and you should only reach for it when EXECUTE IMMEDIATE cannot do the job.

When you need DBMS_SQL

  • Unknown result shape. You do not know how many columns the query returns until you parse it. EXECUTE IMMEDIATE requires the INTO clause to match a known set of variables.
  • Dynamic bind arrays. You want to fetch a batch of rows at once with bulk operations, and the number of values is not fixed.
  • Statements built in pieces. Some tools construct SQL text incrementally across multiple procedures — DBMS_SQL supports parse-then-runtime-bind patterns that EXECUTE IMMEDIATE cannot.
  • Dynamic cursors that need re-parsing. If the plan should be recalculated on each execution rather than cached, DBMS_SQL gives you explicit control.

The basic pattern

DECLARE
    v_cursor  INTEGER;
    v_sql     VARCHAR2(200);
    v_count   NUMBER;
BEGIN
    v_sql    := 'SELECT COUNT(*) FROM orders WHERE customer_id = :cid';
    v_cursor := DBMS_SQL.OPEN_CURSOR;

    DBMS_SQL.PARSE(v_cursor, v_sql, DBMS_SQL.NATIVE);
    DBMS_SQL.BIND_VARIABLE(v_cursor, ':cid', 'C001');
    DBMS_SQL.DEFINE_COLUMN(v_cursor, 1, v_count);

    IF DBMS_SQL.EXECUTE(v_cursor) > 0 THEN
        IF DBMS_SQL.FETCH_ROWS(v_cursor) > 0 THEN
            DBMS_SQL.COLUMN_VALUE(v_cursor, 1, v_count);
        END IF;
    END IF;

    DBMS_SQL.CLOSE_CURSOR(v_cursor);
    DBMS_OUTPUT.PUT_LINE('Order count: ' || v_count);
END;

That is the same query as the EXECUTE IMMEDIATE example earlier, but with every step explicit. It is more verbose, and for the same result. The verbosity is the price for the extra control.

The rule of thumb

If EXECUTE IMMEDIATE can run your dynamic SQL, use it. It is shorter, easier to read, less error-prone, and just as safe when you use bind variables. Only reach for DBMS_SQL when the SQL cannot be expressed in the INTO / USING / RETURNING INTO shape that EXECUTE IMMEDIATE requires — typically when the number of columns or binds is unknown until runtime.

Bind Variables and SQL Injection

SQL injection happens when user-supplied text is treated as SQL rather than as a value. The attacker supplies text that closes the current statement and starts their own command. The database does exactly what the text says.

The vulnerable pattern

-- NEVER DO THIS
v_sql := 'SELECT * FROM customers WHERE customer_name = ''' || p_name || '''';
EXECUTE IMMEDIATE v_sql;

If the user supplies Alice Corp, the query runs as expected. If they supply Alice Corp' OR '1'='1, the SQL text becomes:

SELECT * FROM customers WHERE customer_name = 'Alice Corp' OR '1'='1'

The condition '1'='1' is always true, so the query returns every row in the table. Worse things are possible — attackers can chain additional statements, drop tables, or read from tables they should not have access to.

The safe pattern

-- Safe: use a bind variable
v_sql := 'SELECT * FROM customers WHERE customer_name = :name';
EXECUTE IMMEDIATE v_sql USING p_name;

The bind variable is passed to the database as a separate value. Whatever the user supplies is treated as data — it cannot close the string and start new SQL, because it is never part of the SQL text in the first place.

If the user supplies Alice Corp' OR '1'='1, the database looks for a customer named literally that. It finds nothing, which is the correct behaviour. The attacker's text is data, not code.

Why it also helps performance

Bind variables are not just a security measure. They let the database cache the execution plan across executions. If you build the same query with a different literal every time, the database sees a new SQL string and parses it fresh. With bind variables, the SQL text is identical and the plan is reused.

-- Without binds: the database sees five different SQL statements
'SELECT * FROM orders WHERE customer_id = ''C001'''
'SELECT * FROM orders WHERE customer_id = ''C002'''
'SELECT * FROM orders WHERE customer_id = ''C003'''

-- With binds: one SQL statement, plan reused
'SELECT * FROM orders WHERE customer_id = :cid'

On systems running thousands of dynamic queries per second, that plan reuse is a measurable difference in CPU usage.

Handling Table and Column Names Safely

Bind variables have one big limitation: they cannot be used for identifiers. Table names and column names have to be part of the SQL text because the database needs to know them at parse time. That means you cannot bind them, and you have to be careful about how you concatenate them.

Why the identifier is different from a value

-- This works — p_customer_id is a value
v_sql := 'SELECT * FROM customers WHERE customer_id = :id';
EXECUTE IMMEDIATE v_sql USING p_customer_id;

-- This does NOT work — :table_name cannot be bound
v_sql := 'SELECT COUNT(*) FROM :table_name';   -- syntax error

The database parses the SQL text to know which table to read. A bind variable is not known until later in the pipeline, so it cannot be used where an identifier is expected.

The safe approach — whitelist validation

DECLARE
    v_table_name VARCHAR2(30);
    v_sql        VARCHAR2(200);
BEGIN
    -- Step 1: validate the identifier against a known-good list
    IF p_table_name IN ('CUSTOMERS', 'ORDERS', 'ACCOUNTS') THEN
        v_table_name := p_table_name;
    ELSE
        RAISE_APPLICATION_ERROR(-20001, 'Invalid table name');
    END IF;

    -- Step 2: concatenate the validated identifier
    v_sql := 'SELECT COUNT(*) FROM ' || v_table_name;
    EXECUTE IMMEDIATE v_sql INTO v_count;
END;

The identifier goes through a check against a fixed list of allowed values. If it matches, it can be safely concatenated. If it does not, the procedure raises an error and stops. This is the standard way to allow a user to specify a table without opening the door to injection.

Using DBMS_ASSERT for extra safety

v_table_name := DBMS_ASSERT.SIMPLE_SQL_NAME(p_table_name);

Oracle provides the DBMS_ASSERT package for validating identifiers. SIMPLE_SQL_NAME throws an exception if the input contains anything that is not a valid unquoted SQL identifier. It is a good second line of defence on top of whitelisting, and worth using whenever the identifier comes from outside the program.

Column names are worse than table names

Table names have a finite set that you can list. Column names can be more varied, and listing every valid column of every table is tedious. The whitelist still works — build a map of allowed columns per table and check the user's choice against it. The alternative — trusting the input — is not worth the risk.

Running DDL from PL/SQL

PL/SQL does not let you write DDL directly inside a procedure. You cannot put CREATE TABLE, DROP TABLE, ALTER TABLE, or TRUNCATE TABLE in the body of a procedure and expect it to compile.

Why static DDL is not allowed

PL/SQL is designed to run against a fixed schema. The compiler needs to know what tables and columns exist so it can resolve references and produce dependency information. A CREATE TABLE statement would change the schema in ways that could invalidate the very procedure issuing it. Rather than try to reason about those cases, PL/SQL simply forbids DDL in static code.

The workaround — EXECUTE IMMEDIATE

BEGIN
    EXECUTE IMMEDIATE 'CREATE TABLE temp_orders (
        order_id   NUMBER,
        order_date DATE,
        amount     NUMBER(12,2)
    )';
END;

The DDL text is passed as a string to EXECUTE IMMEDIATE. The database parses it at runtime, executes it, and the procedure compiles normally because the compiler never sees the DDL. All other DDL works the same way.

The DDL that most needs this treatment

  • CREATE TABLE, CREATE INDEX, CREATE VIEW
  • ALTER TABLE, ALTER INDEX
  • DROP TABLE, DROP INDEX
  • TRUNCATE TABLE — fast way to empty a table
  • GRANT, REVOKE — permission changes

Do not forget that DDL commits

BEGIN
    EXECUTE IMMEDIATE 'DROP TABLE temp_orders';
    -- This DDL commits. The transaction before it is now permanent.
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;   -- This does NOT undo the DROP
END;

On Oracle, DDL statements auto-commit before and after running. That means any pending DML in the current transaction becomes permanent as a side effect, and the DDL itself cannot be rolled back. Be careful about issuing DDL from inside a transaction that has pending changes you were planning to roll back.

Debugging Dynamic SQL

Dynamic SQL is harder to debug than static SQL. There is no compile-time check, so syntax errors and typos only surface when the statement runs. That makes printing the generated SQL before you execute it a standard practice.

The DBMS_OUTPUT.PUT_LINE trick

DECLARE
    v_sql VARCHAR2(400);
BEGIN
    v_sql := 'SELECT customer_name, credit_limit '
          || 'FROM customers '
          || 'WHERE region = :region '
          || 'AND credit_limit > :limit';

    -- Print the SQL text before running it
    DBMS_OUTPUT.PUT_LINE(v_sql);

    EXECUTE IMMEDIATE v_sql
        INTO v_name, v_limit
        USING 'North', 50000;
END;

The output shows exactly what the SQL text looks like when the database sees it. Missing spaces, extra quotes, wrong casing — all visible immediately. Nine out of ten dynamic SQL bugs are visible from the printed text.

Enable DBMS_OUTPUT in your client:

-- SQL*Plus
SET SERVEROUTPUT ON SIZE UNLIMITED

-- SQL Developer
-- Turn on the DBMS Output panel and connect to the session

Logging to a table

INSERT INTO dyn_sql_log (log_id, sql_text, logged_at, session_user_id)
VALUES (dyn_sql_log_seq.NEXTVAL, v_sql, SYSDATE, USER);
COMMIT;

When dynamic SQL runs in a scheduled job or an unattended process, DBMS_OUTPUT does not help — nobody is watching the session. Writing the SQL text to a logging table lets you trace what happened after the fact.

Common issues to look for

  • Missing spaces. 'FROM customers' || 'WHERE' produces FROM customersWHERE. Always start or end each fragment with a space.
  • Quoted strings confused with identifiers. Concatenating a value without quotes makes the parser treat it as a column name.
  • Missing bind markers. If the USING clause has three values but the SQL has two markers, you get a runtime error about a value count mismatch.
  • Reserved word collisions. A column named date or number needs to be quoted or aliased.

Performance Considerations

Dynamic SQL is not free. Every execution parses the SQL text, plans the query, and runs it. With bind variables, the plan is cached and reused — the parse is skipped on subsequent runs. Without bind variables, every execution is a fresh parse.

The soft parse vs hard parse distinction

  • Hard parse. The database has never seen this SQL text before. It parses, plans, and caches the result. Expensive.
  • Soft parse. The database finds the same SQL text in its shared pool. It reuses the plan. Cheap — almost free.
-- Hard parse every time
v_sql := 'SELECT * FROM customers WHERE customer_id = ''' || p_id || '''';

-- Soft parse after the first execution
v_sql := 'SELECT * FROM customers WHERE customer_id = :id';

When bind variables are not possible

Identifiers have to be concatenated, so they cannot use binds. That means two dynamic queries against different tables are two different SQL statements with two different plans. This is unavoidable, but you can reduce the cost by:

  • Keeping the number of distinct table names small — a whitelist enforces this anyway.
  • Building the SQL text only when necessary, not on every row of a loop.
  • Using EXECUTE IMMEDIATE rather than DBMS_SQL when possible — DBMS_SQL has more overhead per execution.

Bulk operations

-- FORALL for bulk DML
FORALL i IN 1 .. v_ids.COUNT
    EXECUTE IMMEDIATE 'UPDATE accounts SET balance = balance - 10 WHERE account_id = :id'
        USING v_ids(i);

Actually — that is not quite right. FORALL does not support EXECUTE IMMEDIATE. The correct form for bulk DML is to use static SQL inside FORALL, or to use DBMS_SQL with BIND_ARRAY:

-- Static SQL inside FORALL: preferred when the SQL text is fixed
FORALL i IN 1 .. v_ids.COUNT
    UPDATE accounts SET balance = balance - 10 WHERE account_id = v_ids(i);

The lesson: bulk operations and dynamic SQL do not always mix well. If the SQL text can be static, use static SQL and take the bulk optimisations. If it has to be dynamic, look at whether a single dynamic statement with an IN list would do the job.

The performance summary

  • Use bind variables → plan reuse → cheap subsequent executions.
  • Concatenate values → new SQL text every time → hard parse every time.
  • Concatenate identifiers → new SQL text for each name → hard parse for each name.
  • Use EXECUTE IMMEDIATE instead of DBMS_SQL unless you need the extra control.

18 Interview Questions and Answers

These are the dynamic SQL questions that come up most often. Every answer stands on its own — read them in any order, or work through them in sequence to build up the full picture.

🔍

No matching questions found

Try a keyword like EXECUTE, injection, or binds.

Dynamic SQL is SQL that is constructed at runtime rather than written directly into the code. You build the SQL text as a string — usually inside a PL/SQL procedure — and hand it to the database to parse and execute.

BEGIN
    EXECUTE IMMEDIATE 'SELECT * FROM orders WHERE customer_id = ''C001''';
END;

That statement will run, but it is a bad example — the customer id is hard-coded into the SQL text. Real dynamic SQL uses bind variables:

BEGIN
    EXECUTE IMMEDIATE 'SELECT * FROM orders WHERE customer_id = :cid'
        USING 'C001';
END;

Dynamic SQL is used whenever the shape of the query depends on runtime inputs. A report tool where the user picks the columns. An admin script that operates on a table name passed in from configuration. A DDL statement that PL/SQL otherwise cannot issue. A query whose WHERE clause grows or shrinks based on which filters the user supplied. None of those can be expressed in static SQL.

The trade-off is real. Dynamic SQL is harder to read, harder to debug, and opens the door to injection if you are not careful. Use it when you have to and never when a static query would do the same job.

Flexibility is the main one. Dynamic SQL lets you build queries whose table names, column names, filters, and even join structures depend on values that are only known at runtime.

Four concrete benefits:

  • Dynamic object names. A procedure can operate on any table whose name is passed in. Useful for utilities and admin tools.
  • Dynamic column lists. A report tool can let the user choose which columns to display.
  • Conditional filters. A search screen where the user fills in zero to ten criteria — dynamic SQL builds a query that matches exactly what they asked for.
  • DDL from PL/SQL. Statements like CREATE TABLE and ALTER TABLE cannot be written directly inside PL/SQL. Dynamic SQL is the only way to issue them from a procedure.
-- Example: dynamic table creation
BEGIN
    EXECUTE IMMEDIATE 'CREATE TABLE temp_results (
        run_id     NUMBER,
        result_val NUMBER
    )';
END;

That statement creates a table at runtime. Static PL/SQL cannot do it. Every DDL helper tool, every schema migration script, every test setup utility relies on this pattern.

Oracle offers two mechanisms, and the choice between them depends on how complex the dynamic SQL needs to be.

EXECUTE IMMEDIATE is the preferred tool for most cases. It is a single PL/SQL statement that parses and runs one dynamic SQL statement. It handles SELECT, INSERT, UPDATE, DELETE, and DDL, and supports bind variables through the USING clause.

DECLARE
    v_query VARCHAR2(200);
BEGIN
    v_query := 'UPDATE orders SET order_status = :status WHERE order_id = :id';
    EXECUTE IMMEDIATE v_query USING 'Delivered', 1004;
END;

DBMS_SQL is a procedural package for complex scenarios. It gives you a cursor you control step by step — open, parse, bind, execute, fetch, close. The extra control matters when the number of columns or binds is unknown until runtime.

DECLARE
    v_cursor INTEGER;
    v_query  VARCHAR2(200);
BEGIN
    v_query  := 'SELECT COUNT(*) FROM orders WHERE customer_id = :cid';
    v_cursor := DBMS_SQL.OPEN_CURSOR;
    DBMS_SQL.PARSE(v_cursor, v_query, DBMS_SQL.NATIVE);
    DBMS_SQL.BIND_VARIABLE(v_cursor, ':cid', 'C001');
    -- fetch, define, execute, close ...
END;

The rule: use EXECUTE IMMEDIATE whenever it can do the job. Reach for DBMS_SQL only when the statement's shape is truly unknown at compile time.

Bind variables. Every value that comes from outside the program goes through a bind, never through string concatenation. That is the rule and it has no exceptions.

-- VULNERABLE: user input becomes part of the SQL text
v_sql := 'SELECT * FROM customers WHERE customer_name = ''' || p_name || '''';

-- SAFE: user input is passed as a value, not as SQL
v_sql := 'SELECT * FROM customers WHERE customer_name = :name';
EXECUTE IMMEDIATE v_sql USING p_name;

The difference is subtle but total. In the first version, if the user supplies Alice Corp' OR '1'='1, the SQL text becomes WHERE customer_name = 'Alice Corp' OR '1'='1' — which is always true and returns every row. In the second version, the same input is treated as a literal string. The database looks for a customer whose name is literally Alice Corp' OR '1'='1, which does not exist, so no rows come back.

Bind variables also help performance. The SQL text stays the same across executions, so the database caches and reuses the execution plan instead of parsing fresh every time.

The one thing bind variables cannot handle is object names — table names and column names. Those have to be concatenated into the SQL text. For identifiers, validate against a whitelist before using them.

Five limitations come up regularly. None of them are deal-breakers, but all of them shape when dynamic SQL is the right tool.

Harder to read and debug. Static SQL is visible in the source code. Dynamic SQL only exists as text at runtime, so you have to print it or log it to see what was actually executed.

No compile-time syntax checking. The compiler does not parse the SQL text because it does not exist yet. Syntax errors, missing tables, wrong column names — all surface as runtime errors.

Performance cost without binds. Concatenating values into the SQL text produces a different SQL statement every time, so the database has to parse and plan it fresh. Bind variables fix this, but only for values.

Explicit permission handling. The database checks permissions at runtime, not at compile time. If the executing user lacks SELECT on a table the dynamic SQL references, the statement fails only when it runs.

-- This fails at runtime, not at compile time
v_sql := 'SELECT * FROM table_the_user_cannot_read';   -- compiles fine
EXECUTE IMMEDIATE v_sql;                                -- fails

SQL injection risk. Any time user input flows into the SQL text, there is a risk. Bind variables mitigate this, but they do not cover identifiers.

EXECUTE IMMEDIATE is a single statement. DBMS_SQL is a package of procedures. The first runs one dynamic SQL statement end to end. The second gives you a cursor you control step by step.

Aspect EXECUTE IMMEDIATE DBMS_SQL
Complexity Simple Verbose
Best for Single statements with known shape Dynamic cursors, unknown shapes
Returns a result Via INTO Via FETCH into a define buffer
Number of columns Must be known at compile time Can be discovered at runtime
Bulk operations Limited Full BIND_ARRAY support
Plan caching Yes, with binds Yes, with binds

Use EXECUTE IMMEDIATE whenever it can express the dynamic SQL. That covers the vast majority of cases — DDL, DML with a known set of columns, queries with a fixed result shape.

Reach for DBMS_SQL only when the statement's shape is not known until runtime. A query whose column list comes from the database metadata. A bind list that varies by call. Bulk operations with dynamically-sized arrays.

Oracle PL/SQL does not let you write DDL directly inside a procedure. Statements like CREATE TABLE, ALTER TABLE, and DROP TABLE fail to compile because PL/SQL is designed to run against a fixed schema.

The workaround is to pass the DDL text to EXECUTE IMMEDIATE:

BEGIN
    EXECUTE IMMEDIATE 'ALTER TABLE customers ADD (contact_email VARCHAR2(100))';
END;

The database parses and runs the DDL at runtime. The procedure compiles normally because the compiler never sees the DDL inside its body.

One important caveat — DDL auto-commits. On Oracle, running any DDL statement commits the current transaction as a side effect, and the DDL itself cannot be rolled back. If your procedure has pending DML that you were planning to roll back, the DDL will commit it anyway.

BEGIN
    UPDATE customers SET credit_limit = 0 WHERE customer_id = 'C001';
    -- The next statement commits the update above, even before it runs
    EXECUTE IMMEDIATE 'CREATE TABLE temp_table (id NUMBER)';
    ROLLBACK;   -- Does not undo the CREATE TABLE or the earlier UPDATE
END;

That is one reason DDL-heavy procedures are usually structured to run DDL first, then DML, or to run the whole thing in a context where a partial failure is not catastrophic.

Build the WHERE clause incrementally, adding each condition only when the corresponding parameter is supplied. A common trick is to start with WHERE 1=1 so every subsequent fragment can start with AND without worrying whether it is the first condition.

DECLARE
    v_query      VARCHAR2(400);
    v_dept_id    NUMBER;
    v_min_salary NUMBER;
BEGIN
    v_query := 'SELECT customer_name FROM customers WHERE 1=1';

    IF v_region IS NOT NULL THEN
        v_query := v_query || ' AND region = :region';
    END IF;

    IF v_min_credit IS NOT NULL THEN
        v_query := v_query || ' AND credit_limit >= :min_credit';
    END IF;

    EXECUTE IMMEDIATE v_query USING v_region, v_min_credit;
END;

Two things are worth noting. First, every value goes through a bind variable — nothing is concatenated directly. Second, the USING clause has to list bind values in the order the markers appear in the text. If a marker is added conditionally, the corresponding value has to be added in the same order.

The pattern scales to any number of optional conditions. Search screens with ten possible filters all build the same way.

The single most useful technique is to print the SQL text before you run it. Because the SQL is built at runtime, the source code does not show what was actually executed. Printing it does.

DECLARE
    v_query VARCHAR2(400);
BEGIN
    v_query := 'SELECT * FROM customers WHERE region = :region';
    -- Print before executing
    DBMS_OUTPUT.PUT_LINE('Generated SQL: ' || v_query);
    EXECUTE IMMEDIATE v_query USING 'North';
END;

Enable DBMS_OUTPUT in your client first. In SQL*Plus:

SET SERVEROUTPUT ON SIZE UNLIMITED

In SQL Developer, open the DBMS Output panel and connect it to the session.

For jobs that run unattended — nightly batches, scheduled procedures — printing to the console is useless because nobody is watching. Log the SQL text to a table instead:

INSERT INTO dyn_sql_log (sql_text, logged_at, session_user_id)
VALUES (v_query, SYSDATE, USER);
COMMIT;

Then query the log the next morning when something went wrong. That is often the only way to trace what a runtime-generated statement was actually doing.

The most common bugs you will find this way: missing spaces between concatenated fragments, values that were not properly quoted, bind markers that do not match the USING clause, and reserved words used as column names.

Dynamic SQL shows up in a handful of recurring scenarios. Recognising them makes it easier to decide when to reach for it.

Dynamic report generation. A reporting tool that lets the user choose columns, filters, groupings, and sort orders. Static SQL cannot express an unknown column list.

v_sql := 'SELECT ' || p_columns || ' FROM orders WHERE ' || p_filter;

Administrative utilities. A script that operates on any table whose name comes from configuration. Table statistics collection, index rebuilds, data exports — all of them need the target table name at runtime.

Schema management. Procedures that create, alter, or drop tables as part of a migration. Anything that issues DDL has to go through dynamic SQL because PL/SQL does not support DDL directly.

Dynamic column selection. A query that picks which columns to return based on a runtime variable:

DECLARE
    v_column VARCHAR2(50) := 'salary';
    v_sql    VARCHAR2(200);
BEGIN
    v_sql := 'SELECT ' || v_column || ' FROM employees';
    EXECUTE IMMEDIATE v_sql;
END;

Data movement between tables. ETL jobs where the source and destination tables are chosen from a config table. Copy the data, apply the transformation, insert into the target — all driven by runtime names.

In every one of those cases, the same discipline applies: values go through bind variables, identifiers go through a whitelist, and the SQL text is logged or printed for debugging.

No. Bind variables are for values, not identifiers. The database has to know the table name at parse time so it can build a plan. A bind variable's value is not known until after parsing, so it cannot be used where an identifier is expected.

-- This is a syntax error
v_sql := 'SELECT COUNT(*) FROM :table_name';
EXECUTE IMMEDIATE v_sql USING 'customers';

-- Identifiers have to be concatenated
v_sql := 'SELECT COUNT(*) FROM ' || v_table_name;

That leaves dynamic SQL with a problem: the identifier comes from outside the program (usually from a user or a config file), and it goes into the SQL text without protection. This is where SQL injection lives when values are properly bound.

The solution is validation against a whitelist. Build a list of valid table names and check the input against it:

IF p_table_name IN ('CUSTOMERS', 'ORDERS', 'ACCOUNTS') THEN
    v_table_name := p_table_name;
ELSE
    RAISE_APPLICATION_ERROR(-20001, 'Invalid table name: ' || p_table_name);
END IF;

If the input matches, it can be safely concatenated. If it does not, the procedure stops with an error before any SQL runs.

Oracle also provides DBMS_ASSERT.SIMPLE_SQL_NAME as an additional check. It throws an exception if the string contains anything that is not a valid SQL identifier. Combined with a whitelist, that is a strong defence.

Native dynamic SQL is the PL/SQL feature that lets you build and run SQL statements at runtime without going through the DBMS_SQL package. The syntax is direct:

BEGIN
    EXECUTE IMMEDIATE 'SELECT ...';
END;

It was introduced in Oracle 8i to replace the more verbose DBMS_SQL approach. The "native" part means it is built into the PL/SQL language itself, not a package you have to call.

Native dynamic SQL covers three main forms:

  • EXECUTE IMMEDIATE — the general-purpose statement for any dynamic SQL, with INTO, USING, and RETURNING INTO clauses.
  • OPEN FOR — opens a dynamic cursor you can fetch from one row at a time.
  • Dynamic bulk operations — EXECUTE IMMEDIATE with BULK COLLECT INTO for fetching many rows at once.
-- OPEN FOR pattern: dynamic cursor
DECLARE
    v_cursor  SYS_REFCURSOR;
    v_name    VARCHAR2(100);
BEGIN
    OPEN v_cursor FOR 'SELECT customer_name FROM customers WHERE region = :r' USING 'North';
    LOOP
        FETCH v_cursor INTO v_name;
        EXIT WHEN v_cursor%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(v_name);
    END LOOP;
    CLOSE v_cursor;
END;

That pattern handles dynamic queries that return multiple rows with an unknown number of results. It is a middle ground between EXECUTE IMMEDIATE (which needs a single row into a fixed INTO list) and DBMS_SQL (which gives you the most control but the most code).

For 99% of dynamic SQL tasks, native dynamic SQL is enough. The remaining 1% — truly unknown column counts, dynamic bind arrays — needs DBMS_SQL.

If user input flows into the SQL text without a bind variable, you have a SQL injection vulnerability. The user can supply text that changes the meaning of the query.

-- Vulnerable
v_sql := 'SELECT * FROM customers WHERE customer_name = ''' || p_name || '''';

Attackers exploit this in three common ways.

Bypassing filters. Supply Alice Corp' OR '1'='1 and the query returns every row in the table. Useful for reading data the attacker should not be able to access.

Union-based extraction. Supply text that closes the current query and unions in another one. Read data from tables the application never intended to expose.

Stacked statements. Some databases allow multiple statements in one string. The attacker appends ; DROP TABLE customers; -- and the database runs both.

v_sql := 'SELECT * FROM customers WHERE customer_name = '''
         || p_name
         || '''';

-- With p_name = "x'; DROP TABLE orders; --"
-- The SQL becomes:
-- SELECT * FROM customers WHERE customer_name = 'x';
-- DROP TABLE orders; --'

Whether the attack is possible depends on the database's configuration, but the defence is the same everywhere — never concatenate values. Use bind variables.

There is one scenario where concatenation cannot be avoided: identifiers. Table names and column names have to be part of the SQL text. For those, whitelist validation is the only safe approach.

The bind variable IN list is a classic problem in dynamic SQL. You cannot bind a variable number of values in a single bind variable:

-- This does NOT work
WHERE customer_id IN (:ids);   -- :ids would be treated as a single value

Three approaches handle it. The choice depends on the number of values and how much you care about plan reuse.

Approach 1 — concatenate the values (safe if they are not user-supplied).

v_sql := 'SELECT * FROM customers WHERE customer_id IN (';
v_sql := v_sql || '''' || 'C001' || ''', ''' || 'C002' || '''';
v_sql := v_sql || ')';

Fast to write, but only safe when the values come from a trusted source — a hard-coded list, a previous query result. Never from user input.

Approach 2 — build one bind marker per value.

v_sql := 'SELECT * FROM customers WHERE customer_id IN (';
FOR i IN 1 .. v_ids.COUNT LOOP
    v_sql := v_sql || ':id' || i;
    IF i < v_ids.COUNT THEN
        v_sql := v_sql || ', ';
    END IF;
END LOOP;
v_sql := v_sql || ')';

EXECUTE IMMEDIATE v_sql USING v_ids(1), v_ids(2), v_ids(3);

Safe and reusable. The downside is that the number of binds changes the SQL text, so a list of three values and a list of four are different statements with different plans.

Approach 3 — use a collection with a table function.

CREATE TYPE id_list_t AS TABLE OF VARCHAR2(10);

SELECT * FROM customers
WHERE customer_id IN (SELECT COLUMN_VALUE FROM TABLE(id_list_t('C001', 'C002', 'C003')));

Single SQL text for any number of values. Plan reuse is best. The setup is more involved — you have to define the collection type and construct it in the calling code — but for hot queries it is worth it.

Dynamic SQL runs inside the same transaction as the calling code. If you are in a transaction and issue an EXECUTE IMMEDIATE that performs an INSERT, that insert is part of the transaction. A subsequent ROLLBACK undoes it, just like a static insert.

BEGIN
    EXECUTE IMMEDIATE 'INSERT INTO orders (order_id, customer_id) VALUES (1008, ''C001'')';
    -- Still inside the transaction
    ROLLBACK;   -- The insert is undone
END;

The exception is DDL. On Oracle, DDL statements auto-commit the transaction before they run and again after they finish. If you have pending DML and then run DDL through dynamic SQL, the pending DML commits first.

BEGIN
    INSERT INTO customers (customer_id, customer_name) VALUES ('C008', 'Henry Co');
    EXECUTE IMMEDIATE 'CREATE TABLE temp_table (id NUMBER)';
    -- The INSERT above is now committed, even though we have not issued COMMIT
    ROLLBACK;   -- Does not undo the INSERT or the CREATE TABLE
END;

That behaviour is specific to DDL. DML through dynamic SQL is transactionally identical to static DML.

There is also an autocommit option in some client tools (SQL*Plus with SET AUTOCOMMIT ON) that commits after every statement. Do not confuse that with DDL behaviour — it is a client setting, not a database rule.

Every distinct SQL text that the database parses gets an entry in the shared pool — Oracle's cache of parsed statements and plans. Dynamic SQL can churn that cache if you are not careful, because a slightly different SQL text creates a different entry.

-- Each of these produces a different entry
'SELECT * FROM orders WHERE customer_id = ''C001'''
'SELECT * FROM orders WHERE customer_id = ''C002'''
'SELECT * FROM orders WHERE customer_id = ''C003'''
-- ...thousands more, one per distinct customer id

On a busy system, that churn evicts useful plans from the shared pool and forces other queries to re-parse. It is called "shared pool fragmentation" and it is a real performance problem.

Bind variables fix it. The SQL text stays the same, so it is one shared pool entry reused for every customer id:

'SELECT * FROM orders WHERE customer_id = :cid'

The remaining source of churn is identifiers. Table names and column names cannot be bound, so a utility that operates on twenty tables produces twenty different SQL texts. That is usually acceptable because the utility is not called a million times per second — but it is worth knowing about.

If you see "hard parse" counts climbing and the shared pool filling up, dynamic SQL without binds is a common cause. Switching to binds is often a one-line fix that resolves the problem.

Permissions on dynamic SQL are checked at runtime, not at compile time. Static SQL fails to compile if the executing user lacks SELECT on a referenced table. Dynamic SQL compiles fine and fails only when the statement runs.

-- Compiles fine even if the user cannot read this table
v_sql := 'SELECT * FROM sensitive_data';
EXECUTE IMMEDIATE v_sql;   -- Fails at runtime with ORA-00942

The exceptions you can see:

  • ORA-00942 — table or view does not exist (or the user cannot see it).
  • ORA-01031 — insufficient privileges.
  • PLS-00905 — object is invalid or the user does not have execute permission on a referenced PL/SQL unit.

Two ways to grant permissions for dynamic SQL.

Direct grant. Grant the executing user the privileges they need on the underlying objects. Simple, but every user of the procedure needs the same grants.

Definer's rights. Declare the procedure with AUTHID DEFINER — the default — so that the procedure runs with the privileges of its owner, not the caller. Then the caller does not need direct access to the tables. This is the standard approach for controlled access.

CREATE OR REPLACE PROCEDURE get_customer_count
AUTHID DEFINER
AS
    v_count NUMBER;
BEGIN
    EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM customers' INTO v_count;
    DBMS_OUTPUT.PUT_LINE(v_count);
END;

The alternative is AUTHID CURRENT_USER, where the procedure runs with the caller's privileges. Choose based on whether you want to control access by granting it to the procedure owner or to each caller.

There is a right shape and a wrong shape. The right one uses bind variables for every value, validates identifiers against a whitelist, and logs the SQL text before running it.

CREATE OR REPLACE PROCEDURE customer_count_by_region (
    p_region IN VARCHAR2,
    p_count  OUT NUMBER
) AS
    v_sql VARCHAR2(400);
BEGIN
    -- Use bind variables for all values
    v_sql := 'SELECT COUNT(*) '
          || 'FROM customers '
          || 'WHERE region = :region';

    -- Log for debugging
    DBMS_OUTPUT.PUT_LINE('Executing: ' || v_sql);

    -- Execute with the bind
    EXECUTE IMMEDIATE v_sql
        INTO p_count
        USING p_region;

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

The pieces of that template:

  • Build the SQL text in a variable, not directly in EXECUTE IMMEDIATE — makes it easier to log.
  • Use bind markers (:region) for every value, not string concatenation.
  • Log the SQL before running it, at least during development.
  • Handle exceptions so the procedure does not crash without a trace.

When identifiers are involved, add a whitelist check before the SQL text is built:

IF p_region NOT IN ('North', 'South', 'East', 'West') THEN
    RAISE_APPLICATION_ERROR(-20001, 'Invalid region: ' || p_region);
END IF;

That combination — bind values, whitelist identifiers, log the SQL, handle errors — is the standard shape for safe dynamic SQL. Everything else is a variation on the same template.

Mistakes I Keep Seeing in Interviews

These come up repeatedly. None of them are exotic, and every one of them is easy to avoid once you know it is coming.

Mistake one: concatenating user input into the SQL text instead of using bind variables. The statement runs on test data because the test input is well-behaved. Then real users find it and the vulnerability is live.
Mistake two: assuming bind variables can be used for table names. They cannot — the parser needs the identifier at parse time. Table names have to be validated against a whitelist and concatenated.
Mistake three: forgetting that DDL auto-commits. Any pending DML gets committed as a side effect of running DDL through EXECUTE IMMEDIATE. A careful rollback plan does not survive a DDL statement in the middle.
Mistake four: not printing the SQL text when debugging. The error message says "invalid identifier" but you cannot see which identifier because the SQL was built at runtime. Print it.
Mistake five: reaching for DBMS_SQL when EXECUTE IMMEDIATE would do. DBMS_SQL is more code, more overhead, and more places to make a mistake. Use it only when you need its specific capabilities.
Mistake six: no exception handling around dynamic SQL. A syntax error in the generated SQL crashes the calling procedure with an unhandled exception. Catch and log, at minimum.

Follow-Up Questions Interviewers Ask

Getting the first answer right is only half the interview. What usually decides the outcome is what happens next. Here is what to prepare for on the dynamic SQL topics.

After "What is dynamic SQL?" — expect

  • "What is the difference between static and dynamic SQL?"
  • "When would you use dynamic SQL instead of static SQL?"
  • "What is the main risk of dynamic SQL?"
  • "How does the database parse dynamic SQL differently?"

After "How do you prevent SQL injection?" — expect

  • "Can you use bind variables for table names?"
  • "What do you do when you cannot use a bind variable?"
  • "What is DBMS_ASSERT and how does it help?"
  • "Give me an example of a vulnerable dynamic SQL statement."

After "What is EXECUTE IMMEDIATE?" — expect

  • "What is the difference between EXECUTE IMMEDIATE and DBMS_SQL?"
  • "How do you capture a result from EXECUTE IMMEDIATE?"
  • "How do you pass bind variables to it?"
  • "What happens to the plan when you call it repeatedly?"

After "How do you run DDL from PL/SQL?" — expect

  • "Why can you not write DDL directly in PL/SQL?"
  • "What happens to the transaction when you run DDL?"
  • "Can you roll back a DDL statement?"
  • "What is the difference between DDL and DML with respect to transactions?"

For every question above, prepare a second-level answer. The candidates who pass are not the ones who memorised the first answer — they are the ones who kept going for another thirty seconds and said something useful about why it matters.

Quick Quiz

Three questions to check whether the topics above have landed.

1. What is the best defence against SQL injection in dynamic SQL?
2. Can you bind a table name as a bind variable in dynamic SQL?
3. Why is DDL commonly run through dynamic SQL in PL/SQL?