PL/SQL Control Structures — IF, LOOP, WHILE
- PL/SQL control structures exist to fill the gap SQL cannot — conditional branching on runtime state, iterative processing where each row's result affects the next, and error recovery that continues rather than aborts. Reach for them when set-based SQL genuinely cannot express the logic, not by default.
- IF-THEN-ELSIF is evaluated top-down with short-circuit — first TRUE branch wins, everything else is skipped. NULL in any standard comparison returns NULL, not TRUE. IS NULL and IS NOT NULL are the only correct operators for null checks. No exceptions.
- ELSE branches are not optional in batch processing — they are your audit point for unexpected states. A missing ELSE means unrecognized data silently passes through. Add an ELSE that writes to a persistent audit table in every batch IF block.
- PL/SQL control structures add procedural decision-making and repetition to Oracle's declarative SQL engine
- IF-THEN-ELSIF handles branching; CASE selects from mutually exclusive options
- Three loop types: BASIC (run at least once), WHILE (check first), FOR (known range or cursor)
- PL/SQL uses Three-Valued Logic — NULL compared with = always returns NULL, never TRUE
- FORALL and set-based SQL are 100x faster than RBAR loops for bulk DML operations
- Forgetting EXIT WHEN or cursor cleanup causes ORA-01000 and runaway sessions
Infinite loop consuming all PGA memory
ALTER SYSTEM KILL SESSION '<sid>,<serial#>' IMMEDIATESELECT sql_id, sql_text FROM V$SQL WHERE sql_id = (SELECT sql_id FROM V$SESSION WHERE sid = <sid>)NULL comparison returning unexpected results
SELECT COUNT(*) FROM target_table WHERE status IS NULLDBMS_OUTPUT.PUT_LINE('Value: ' || NVL(TO_CHAR(v_var), 'IS NULL'))ORA-01000 maximum open cursors
ALTER SYSTEM SET open_cursors = 1000 SCOPE = BOTHSELECT sql_text, count(*) FROM V$OPEN_CURSOR GROUP BY sql_text ORDER BY 2 DESCRBAR loop causing table lock contention
SELECT sql_id, executions, ROUND(elapsed_time/1000000, 2) elapsed_secs FROM V$SQL WHERE sql_id = '<sql_id>'SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('<sql_id>', NULL, 'ALLSTATS LAST'))Production Incident
IF v_status = NULL THEN expecting it to evaluate to TRUE when the variable held no value. The reasoning was intuitive: if the variable is empty, the condition should match. In PL/SQL's Three-Valued Logic, that intuition is wrong in a way that leaves no trace — the condition evaluates to NULL, not TRUE, so the entire branch is silently skipped on every row where status was NULL.IF v_status = NULL — regardless of what v_status actually contains — never fires. There was no error, no warning, no log entry. The batch job processed every non-NULL record correctly and silently skipped every NULL-status record. The only indication something was wrong was a £2.3M gap in the reconciliation figures three weeks later.= NULL comparison was replaced with IS NULL. Every <> NULL comparison became IS NOT NULL. An ELSE branch was added to each IF block in the batch procedure, writing an audit record to a dedicated exception table whenever a record did not match any expected state. A post-batch verification query was added to the job scheduler: it counts processed versus unprocessed records and raises an alert if the gap exceeds a configurable threshold. The NULL-status records were identified, manually reviewed, corrected, and reprocessed.Production Debug GuideCommon symptoms and immediate actions for production PL/SQL issues
Standard SQL is declarative — you describe the result you want and the optimizer figures out how to get there. That model works brilliantly for set-based operations, but it breaks down the moment your logic needs to branch on a runtime value, iterate until a condition is met, or recover from a failure mid-stream and continue processing. That is exactly the gap PL/SQL control structures fill.
In production, the stakes around this are not theoretical. A poorly structured loop processing 500,000 rows one at a time generates context switches between the PL/SQL and SQL engines on every single iteration — each one carrying the overhead of a round trip. The redo log swells. Row-level locks accumulate. PGA memory climbs. A batch job that should complete in under a minute ends up holding locks for thirty. The same operation expressed as a single FORALL or a well-structured UPDATE runs entirely inside the SQL engine with an optimized execution plan and no context-switching penalty.
But set-based SQL has its own limits. When each row's outcome determines how the next row should be processed, when business rules span multiple dependent queries, when you need to log partial failures and continue rather than abort — that is where procedural control structures earn their place. The judgment call between reaching for a loop versus restructuring the problem as a single SQL statement is one of the clearest signals separating a mid-level Oracle developer from a senior one.
This guide covers the mechanics, the production patterns, the failure modes, and the performance trade-offs. By the end you will know not just how each structure works, but when to use it, when to avoid it, and what to check first when something goes wrong at 2 a.m.
Conditional Logic: The IF-THEN-ELSIF Branching Model
SQL's declarative model handles set operations elegantly, but it was never designed for branching on runtime state. The moment you need to execute a completely different code path based on a value fetched from the database — not just select a different column value, but run different procedures, apply different validation rules, or write to different tables — you need procedural conditional logic. IF-THEN-ELSIF is how PL/SQL expresses that.
The evaluation model is straightforward: conditions are assessed top-down and the first branch whose condition evaluates to TRUE executes. Every subsequent branch is skipped entirely. This short-circuit behaviour is not just an optimisation — it is the mechanism. It means condition ordering matters. If you place the most expensive condition first and it is rarely TRUE, you are paying that cost on every evaluation. Put your cheapest, most commonly matched condition at the top.
The ELSE clause is where many production batch jobs have quietly failed. Without it, any input state that matches no ELSIF condition is silently ignored. In an interactive application, a missed branch is a minor UX issue. In an overnight batch job processing 300,000 invoices, it means a subset of records is never touched and nobody finds out until month-end reconciliation. The ELSE branch should always be present in batch processing, and it should always write to an audit table — not just to DBMS_OUTPUT, which is lost the moment the session ends.
Null handling is the sharpest edge in this model. PL/SQL inherits Three-Valued Logic from SQL: TRUE, FALSE, and NULL. Any comparison involving NULL using a standard operator returns NULL, not FALSE. IF v_status = NULL does not evaluate to TRUE when v_status is NULL — it evaluates to NULL, which is not TRUE, so the block never fires. This is not an edge case. It is how the language works, and it catches experienced developers who switch from other languages where null comparisons behave differently. The fix is always IS NULL and IS NOT NULL — no exceptions.
-- io.thecodeforge: Conditional branching with proper NULL handling and audit trail DECLARE v_counter NUMBER := 1; v_threshold CONSTANT NUMBER := 5; v_status VARCHAR2(20); v_raw_input VARCHAR2(20) := NULL; -- Simulating a NULL-status record BEGIN -- Demonstrate correct NULL handling — IS NULL, never = NULL IF v_raw_input IS NULL THEN v_status := 'PENDING_REVIEW'; -- In production: INSERT INTO batch_audit (record_key, reason, logged_at) -- VALUES (v_id, 'NULL status on intake', SYSTIMESTAMP); DBMS_OUTPUT.PUT_LINE('NULL input detected — flagged for review'); ELSIF v_raw_input = 'APPROVED' THEN v_status := 'PROCESSING'; ELSIF v_raw_input IN ('REJECTED', 'CANCELLED') THEN v_status := 'CLOSED'; ELSE -- Catch-all: unexpected states must never silently pass through v_status := 'UNKNOWN'; DBMS_OUTPUT.PUT_LINE('WARNING: Unexpected status value — ' || NVL(v_raw_input, 'IS NULL')); END IF; -- IF-THEN-ELSIF driving a WHILE LOOP — realistic batch pattern WHILE v_counter <= v_threshold LOOP IF v_counter < 3 THEN v_status := 'INITIALIZING'; ELSIF v_counter BETWEEN 3 AND 4 THEN v_status := 'PROCESSING'; ELSE v_status := 'FINALIZING'; END IF; DBMS_OUTPUT.PUT_LINE( 'Iteration ' || v_counter || ' | Status: ' || v_status ); v_counter := v_counter + 1; END LOOP; END;
Iteration 1 | Status: INITIALIZING
Iteration 2 | Status: INITIALIZING
Iteration 3 | Status: PROCESSING
Iteration 4 | Status: PROCESSING
Iteration 5 | Status: FINALIZING
- Conditions evaluated top-down — first TRUE match wins, remainder skipped entirely
- NULL compared with any standard operator returns NULL, never TRUE — use IS NULL without exception
- ELSE branch is not optional in batch processing — it is your catch-all audit point
- Order conditions by likelihood or cost — cheapest and most common first reduces unnecessary evaluation
- Short-circuit applies to AND/OR chains too — if A is FALSE in A AND B, B is never evaluated
Iterative Logic: Master the Three Loop Types
The choice of loop type is a correctness decision before it is a performance decision. Each of the three loop types in PL/SQL encodes a specific contract about when the exit condition is evaluated, who manages the counter, and what happens to open cursors when the loop terminates.
The BASIC LOOP executes its body at least once before evaluating the EXIT WHEN condition. This is the right choice when you need to perform an action — read from a queue, generate a sequence value, attempt a connection — and then decide whether to continue. The risk is the infinite loop: if EXIT WHEN is omitted or the condition can never become TRUE, the loop runs until the session is killed or the server runs out of PGA memory. Always add a safety ceiling alongside the business exit condition.
The WHILE LOOP evaluates its condition before the first iteration. If the condition is false going in, the body never executes. This is the right model when you are processing a queue, a file, or an external resource that might already be empty. The discipline required here is ensuring the condition variable is actually modified inside the loop body — a WHILE that checks a variable nobody updates is an infinite loop waiting to happen.
The FOR LOOP is the safest of the three for the simple reason that you cannot accidentally create an infinite loop with it. The bounds are fixed at entry, the counter is implicitly declared and incremented, and the loop exits cleanly when the upper bound is reached or the cursor is exhausted. The Cursor FOR LOOP extends this safety to resource management: the cursor is opened, each row fetched into the loop record variable, and the cursor closed on exit — even if an unhandled exception fires mid-loop. This is the single most effective way to prevent ORA-01000 in production code.
The performance conversation is separate from the loop type conversation. RBAR — Row-By-Agonizing-Row — is the pattern of performing one DML operation per loop iteration when a single SQL statement or FORALL could handle the entire dataset. The cost is context switching: every transition between the PL/SQL engine and the SQL engine has overhead. For 500,000 rows, that overhead accumulates into minutes. FORALL sends the entire collection of DML operations to the SQL engine in one call. The throughput difference is not a small margin — benchmarks consistently show 50x to 100x improvement for pure DML workloads. If the loop body is a single INSERT, UPDATE, or DELETE, rewrite it as FORALL before it ships.
-- io.thecodeforge: Production loop patterns — safe cursors, bulk DML, and exception resilience DECLARE -- Type declarations for BULK COLLECT and FORALL pattern TYPE t_invoice_ids IS TABLE OF invoices.invoice_id%TYPE INDEX BY PLS_INTEGER; TYPE t_amounts IS TABLE OF invoices.amount%TYPE INDEX BY PLS_INTEGER; v_ids t_invoice_ids; v_amounts t_amounts; v_idx PLS_INTEGER := 1; v_max CONSTANT PLS_INTEGER := 3; -- Simulated error table insert (replace with your audit table) PROCEDURE log_error(p_id IN NUMBER, p_msg IN VARCHAR2) IS PRAGMA AUTONOMOUS_TRANSACTION; BEGIN DBMS_OUTPUT.PUT_LINE('ERROR LOG | id=' || p_id || ' | ' || p_msg); -- INSERT INTO batch_errors (record_id, error_msg, logged_at) -- VALUES (p_id, p_msg, SYSTIMESTAMP); COMMIT; END; BEGIN -- --------------------------------------------------------------- -- Pattern 1: Numeric FOR LOOP — counter implicit, bounds fixed -- Use when: known iteration count, no cursor involved -- --------------------------------------------------------------- DBMS_OUTPUT.PUT_LINE('--- Numeric FOR LOOP ---'); FOR i IN 1..v_max LOOP DBMS_OUTPUT.PUT_LINE('Iteration: ' || i); END LOOP; -- --------------------------------------------------------------- -- Pattern 2: BASIC LOOP with safety ceiling -- Use when: must execute at least once before checking exit -- --------------------------------------------------------------- DBMS_OUTPUT.PUT_LINE('--- BASIC LOOP with safety ceiling ---'); LOOP DBMS_OUTPUT.PUT_LINE('Basic loop pass: ' || v_idx); v_idx := v_idx + 1; EXIT WHEN v_idx > v_max; -- Business exit condition -- EXIT WHEN v_idx > 10000; -- Safety ceiling (always add this) END LOOP; -- --------------------------------------------------------------- -- Pattern 3: Cursor FOR LOOP — open/fetch/close all implicit -- Use when: iterating over a query result; safest for cursors -- --------------------------------------------------------------- DBMS_OUTPUT.PUT_LINE('--- Cursor FOR LOOP ---'); FOR rec IN ( SELECT level AS invoice_id, ROUND(DBMS_RANDOM.VALUE(100, 9999), 2) AS amount FROM dual CONNECT BY level <= v_max ) LOOP BEGIN IF rec.amount IS NULL THEN log_error(rec.invoice_id, 'NULL amount — skipped'); CONTINUE; -- Skip this row, process the rest END IF; DBMS_OUTPUT.PUT_LINE( 'Invoice ' || rec.invoice_id || ' | Amount: ' || rec.amount ); EXCEPTION WHEN OTHERS THEN log_error(rec.invoice_id, SQLERRM); -- Loop continues to next row automatically END; END LOOP; -- --------------------------------------------------------------- -- Pattern 4: FORALL — bulk DML, no per-row context switching -- Use when: loop body is a single INSERT/UPDATE/DELETE -- --------------------------------------------------------------- DBMS_OUTPUT.PUT_LINE('--- FORALL bulk DML pattern ---'); -- Populate collections (in production: BULK COLLECT INTO from a query) v_ids(1) := 1001; v_amounts(1) := 500.00; v_ids(2) := 1002; v_amounts(2) := 1250.75; v_ids(3) := 1003; v_amounts(3) := 875.50; -- Single round trip to SQL engine for all three rows -- FORALL i IN 1..v_ids.COUNT -- UPDATE invoices -- SET amount = v_amounts(i), updated_at = SYSTIMESTAMP -- WHERE invoice_id = v_ids(i); DBMS_OUTPUT.PUT_LINE( 'FORALL would process ' || v_ids.COUNT || ' rows in one SQL engine call' ); END;
Iteration: 1
Iteration: 2
Iteration: 3
--- BASIC LOOP with safety ceiling ---
Basic loop pass: 1
Basic loop pass: 2
Basic loop pass: 3
--- Cursor FOR LOOP ---
Invoice 1 | Amount: 4823.17
Invoice 2 | Amount: 312.94
Invoice 3 | Amount: 7651.08
--- FORALL bulk DML pattern ---
FORALL would process 3 rows in one SQL engine call
| Structure | Best Use Case | Key Keyword | Evaluation Timing |
|---|---|---|---|
| IF-THEN-ELSE | Conditional branching based on runtime values — use when different code paths must execute, not just different values be assigned | ELSIF | Top-down at point of entry — first TRUE condition wins, rest skipped |
| BASIC LOOP | When the body must execute at least once before the exit condition is checked — queue draining, retry logic, sequence generation | EXIT WHEN | Manual, inside the loop body — developer is responsible for ensuring the condition is reachable |
| FOR LOOP | Known iteration count or cursor traversal — safest choice because counter and bounds are implicit and infinite loops are impossible | IN / IN REVERSE | Automatic at range entry — bounds are fixed when the loop starts |
| WHILE LOOP | Condition-driven iteration where the body may not need to execute at all — processing until a resource is exhausted or a flag is set | WHILE | Before every iteration including the first — false on entry means zero executions |
| CASE | Selecting from mutually exclusive discrete options — cleaner than a long ELSIF chain for a single variable checked against known values | WHEN | Top-down at point of entry — first matching WHEN clause wins |
🎯 Key Takeaways
- PL/SQL control structures exist to fill the gap SQL cannot — conditional branching on runtime state, iterative processing where each row's result affects the next, and error recovery that continues rather than aborts. Reach for them when set-based SQL genuinely cannot express the logic, not by default.
- IF-THEN-ELSIF is evaluated top-down with short-circuit — first TRUE branch wins, everything else is skipped. NULL in any standard comparison returns NULL, not TRUE. IS NULL and IS NOT NULL are the only correct operators for null checks. No exceptions.
- ELSE branches are not optional in batch processing — they are your audit point for unexpected states. A missing ELSE means unrecognized data silently passes through. Add an ELSE that writes to a persistent audit table in every batch IF block.
- FOR LOOP and Cursor FOR LOOP are the safest defaults. They handle counter management, bounds checking, and cursor lifecycle implicitly. If you need to iterate over a query result, Cursor FOR LOOP eliminates ORA-01000 by design.
- FORALL is not a loop variant — it is a fundamentally different execution model. One round trip to the SQL engine instead of N. If the loop body is a single DML statement, FORALL is the correct tool and the throughput improvement is not marginal.
- Wrap loop bodies in BEGIN...EXCEPTION WHEN OTHERS in batch jobs. Log the failing row's key and error to a persistent table using an AUTONOMOUS_TRANSACTION. One bad record should not abort hours of successful processing.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the difference between a WHILE LOOP and a FOR LOOP in PL/SQL? When would you choose one over the other in a production environment?JuniorReveal
- QCan you explain why comparing a variable to NULL using the '=' operator inside an IF statement fails to return TRUE? What is the correct operator?Mid-levelReveal
- QHow does the CONTINUE statement differ from the EXIT statement inside a loop? Provide a use case for skipping an iteration.Mid-levelReveal
- QWhat is 'RBAR' and why is it considered an anti-pattern when using PL/SQL loops for large data updates?SeniorReveal
- QExplain the 'Short-Circuit' evaluation in PL/SQL IF statements. How does it improve performance in complex boolean conditions?SeniorReveal
- QHow would you use a Cursor FOR LOOP to simplify resource management (opening, fetching, and closing)?Mid-levelReveal
Frequently Asked Questions
What is the difference between a CASE expression and an IF statement in PL/SQL?
The distinction is structural, not stylistic. A CASE expression returns a single value — it can appear on the right side of an assignment, inside a SQL SELECT list, or embedded in another expression. v_grade := CASE WHEN score >= 90 THEN 'A' WHEN score >= 75 THEN 'B' ELSE 'F' END is valid because CASE evaluates to a scalar value.
An IF statement controls execution flow — it executes blocks of procedural statements based on a condition but does not return a value and cannot appear inside an expression. Use CASE when you are selecting a value from a known set of mutually exclusive options, especially when you need that selection inside a SQL statement. Use IF when different code paths need to execute: different procedure calls, different error handling logic, different table writes — anything beyond assigning a single value.
Can I use CONTINUE in a nested loop to skip the inner loop and continue the outer loop?
Not directly — CONTINUE without a label always targets the innermost loop it is physically inside. To affect an outer loop from within an inner loop, label the outer loop and reference that label in the CONTINUE statement:
<<outer_loop>> FOR i IN 1..10 LOOP FOR j IN 1..10 LOOP CONTINUE outer_loop WHEN some_condition; -- This line is skipped when condition is true -- Control jumps to the next iteration of i, not j END LOOP; END LOOP;
Without the label, CONTINUE only skips to the next iteration of the j loop. The label must appear immediately before the LOOP keyword on the outer loop, and the same label name must appear after END LOOP for clarity, though that is optional syntax.
Is a FOR LOOP faster than a WHILE LOOP in PL/SQL?
For equivalent numeric range iteration, the difference is negligible in any real workload — the kind of micro-benchmark difference that disappears entirely against the noise of actual database I/O. The compiler handles both efficiently.
The reason to prefer FOR LOOP over WHILE is not speed — it is safety and readability. FOR LOOP cannot create an infinite loop because the bounds are fixed at entry and the counter is automatic. WHILE LOOP can create an infinite loop if the exit condition variable is never updated inside the body. FOR LOOP's intent is immediately clear from the range declaration. Choose based on which model fits the problem correctly, then let the optimizer handle the rest.
How do I handle exceptions inside a loop without aborting the entire batch?
Wrap the loop body in a nested BEGIN...EXCEPTION block. The exception handler catches the error for the current row, logs it to a persistent error table, and the loop continues to the next iteration:
FOR rec IN (SELECT * FROM invoices_to_process) LOOP BEGIN process_invoice(rec.invoice_id, rec.amount); EXCEPTION WHEN OTHERS THEN log_batch_error(rec.invoice_id, SQLCODE, SQLERRM); -- Loop continues automatically END; END LOOP;
The error logging procedure should use PRAGMA AUTONOMOUS_TRANSACTION so the log record commits independently of the outer transaction — otherwise a rollback would erase your error evidence. Commit the main transaction in batches — every 1,000 rows using MOD(v_row_count, 1000) = 0 — to bound undo/redo pressure and preserve partial progress if the job fails partway through.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.