PL/SQL adds procedural logic (if, loop, error handling) on top of SQL
Every program is a block: DECLARE (optional), BEGIN (required), EXCEPTION (optional), END
Variables use %TYPE to automatically match column definitions — avoids hardcoded types
Blocks run inside the database engine, cutting network round trips by up to 100x
Biggest mistake: forgetting SET SERVEROUTPUT ON — DBMS_OUTPUT.PUT_LINE silently discards output
Plain-English First
SQL is like giving orders at a counter: 'Give me all customers from London.' One instruction, one response. PL/SQL is like handing the counter a recipe card: 'Look up customers from London, if there are more than 100 send a report, otherwise send a reminder — and repeat this every Monday.' PL/SQL gives SQL the ability to make decisions, loop, and remember state.
SQL is declarative — you describe what data you want and the database figures out how to get it. But real-world database logic often requires conditional branching, loops, error handling, and reusable procedures. That's what PL/SQL adds.
PL/SQL (Procedural Language extension to SQL) is Oracle's procedural extension to SQL. It runs inside the Oracle database engine itself, which means it avoids the round-trip overhead of sending individual SQL statements from an application. A loop that issues 1000 SQL statements from Java sends 1000 network requests. The same loop in PL/SQL sends one.
By the end of this article you'll understand the PL/SQL block structure, how to use variables, write conditionals and loops, and handle exceptions — the four pillars of every PL/SQL program.
The PL/SQL Block Structure
Every PL/SQL program is a block. Blocks have four sections: DECLARE (optional, for variables), BEGIN (required, the logic), EXCEPTION (optional, error handling), and END. Blocks can be anonymous (run once, not stored) or named (procedures and functions stored in the database). Understanding this structure is the foundation of everything else in PL/SQL.
You'll see the slash (/) at the end — that's what tells SQL*Plus, SQL Developer, and most tools to actually execute the block. Without it, nothing happens.
first_block.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- Anonymous PL/SQL block — runs immediately, not storedDECLARE-- Variable declarations
v_employee_name VARCHAR2(50); -- String up to 50 chars
v_salary NUMBER(10, 2); -- Number with 2 decimal places
v_department_id NUMBER := 10; -- Initialised to 10BEGIN-- Query into variables using SELECT INTOSELECT first_name || ' ' || last_name, salary
INTO v_employee_name, v_salary
FROM employees
WHERE department_id = v_department_id
ANDROWNUM = 1;
-- Output using DBMS_OUTPUT (enable with SET SERVEROUTPUT ON)
DBMS_OUTPUT.PUT_LINE('Employee: ' || v_employee_name);
DBMS_OUTPUT.PUT_LINE('Salary: $' || TO_CHAR(v_salary, '999,999.99'));
EXCEPTION-- Handle the case where no rows are foundWHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No employee found in department ' || v_department_id);
END;
/ -- The slash executes the block in SQL*Plus and SQLDeveloper
Output
Employee: Steven King
Salary: $24,000.00
SET SERVEROUTPUT ON:
Before running any PL/SQL that uses DBMS_OUTPUT.PUT_LINE, run SET SERVEROUTPUT ON in your session. Without it, output is silently discarded and you'll think nothing happened.
Production Insight
Forgetting the slash (/) in SQL*Plus is the #1 reason new PL/SQL devs think their block doesn't work.
Another common pitfall: placing code after the slash — it's never executed.
Rule: always end anonymous blocks with / on its own line.
Key Takeaway
Every PL/SQL program must have BEGIN and END.
DECLARE is for variables, EXCEPTION is for errors.
The slash executes the block — don't forget it.
Variables, Conditions, and Loops
PL/SQL variables are strongly typed. The %TYPE attribute lets you declare a variable that automatically matches a table column's data type — if the column type changes, your variable adapts automatically. %ROWTYPE does the same for an entire row structure. Conditionals use IF/ELSIF/ELSE. Loops come in three flavours: basic LOOP, WHILE, and the most common FOR loop. The FOR loop is preferred for simple iterations because it manages the loop counter implicitly and you don't need a separate variable declaration.
loops_and_conditions.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
DECLARE-- %TYPE anchors variable type to the actual column definition
v_salary employees.salary%TYPE;
v_grade VARCHAR2(10);
v_counter NUMBER;
BEGINSELECT salary INTO v_salary
FROM employees WHERE employee_id = 100;
-- IF / ELSIF / ELSEIF v_salary > 20000THEN
v_grade := 'Executive';
ELSIF v_salary > 10000THEN
v_grade := 'Senior';
ELSIF v_salary > 5000THEN
v_grade := 'Mid-level';
ELSE
v_grade := 'Junior';
ENDIF;
DBMS_OUTPUT.PUT_LINE('Grade: ' || v_grade);
-- FOR loop (most common) — implicit counter, no DECLARE neededFOR v_counter IN1..5LOOP
DBMS_OUTPUT.PUT_LINE('Iteration: ' || v_counter);
ENDLOOP;
END;
/
Output
Grade: Executive
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
Use %TYPE and %ROWTYPE:
Always use employees.salary%TYPE instead of hardcoding NUMBER(8,2). If the column definition changes, your code stays correct automatically. %ROWTYPE declares a variable matching an entire row structure.
Production Insight
Hardcoding data types caused a production outage when a salary column was changed from NUMBER(8,2) to NUMBER(10,2) — all PL/SQL variables sized at 8,2 silently truncated values.
The fix was to replace all hardcoded types with %TYPE.
Rule: never hardcode column types in PL/SQL — use %TYPE or %ROWTYPE.
Key Takeaway
Use %TYPE to anchor variable types to columns.
IF/ELSIF/ELSE handles branching; FOR loops are the cleanest way to iterate.
Avoid hardcoding — let the database drive the types.
Exception Handling in PL/SQL
PL/SQL provides an EXCEPTION block where you can catch and handle errors gracefully. Without it, any runtime error terminates the block and rolls back uncommitted changes. Common built-in exceptions include NO_DATA_FOUND, TOO_MANY_ROWS, DUP_VAL_ON_INDEX, and ZERO_DIVIDE. You can also define your own exceptions using RAISE_APPLICATION_ERROR.
The WHEN OTHERS clause catches every unhandled exception. In production code, every block should include at least WHEN OTHERS to log the error and decide whether to re-raise or continue.
exception_handling.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DECLARE
v_employee_id employees.employee_id%TYPE := 9999;
v_name employees.first_name%TYPE;
BEGINSELECT first_name INTO v_name
FROM employees WHERE employee_id = v_employee_id;
DBMS_OUTPUT.PUT_LINE('Found: ' || v_name);
EXCEPTIONWHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_id || ' not found.');
-- Optionally log or re-raiseWHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('Multiple employees with same ID — data error.');
-- Log for investigationWHENOTHERSTHEN
DBMS_OUTPUT.PUT_LINE('Unexpected error: ' || SQLERRM);
DBMS_OUTPUT.PUT_LINE('Backtrace: ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
RAISE; -- Re-raise after logging; don't hide itEND;
/
Output
Employee 9999 not found.
Don't Use WHEN OTHERS as a Catch-All to Swallow Errors
A blank WHEN OTHERS THEN NULL; hides every error and makes debugging impossible. Always at least log the error and consider re-raising (RAISE;).
Production Insight
A blank WHEN OTHERS caused a batch job to silently produce wrong results for months — every error was swallowed.
When the issue was finally found, thousands of incorrect records had been processed.
Rule: always log in WHEN OTHERS and re-raise unless you have a specific reason not to.
Key Takeaway
Always include exception handling for expected errors (NO_DATA_FOUND, TOO_MANY_ROWS).
Use WHEN OTHERS for logging — never suppress errors.
Use DBMS_UTILITY.FORMAT_ERROR_BACKTRACE to get the full call stack.
Working with Cursors
When you need to process multiple rows from a query, use a cursor. An explicit cursor is declared, opened, fetched, and closed manually. PL/SQL also offers implicit cursors via the FOR loop — the simplest and most common approach for single-table queries. For large row sets, BULK COLLECT speeds processing by fetching many rows at once into a collection, drastically reducing context switches between SQL and PL/SQL.
cursors.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
DECLARE-- Explicit cursor declarationCURSOR emp_cursor ISSELECT employee_id, first_name, salary
FROM employees
WHERE department_id = 10;
v_emp_rec emp_cursor%ROWTYPE;
BEGINOPEN emp_cursor;
LOOPFETCH emp_cursor INTO v_emp_rec;
EXITWHEN emp_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_emp_rec.employee_id || ': ' || v_emp_rec.first_name);
ENDLOOP;
CLOSE emp_cursor;
END;
/
-- Simpler: Implicit cursor FOR loop (recommended)BEGINFOR emp_rec IN (SELECT employee_id, first_name FROM employees WHERE department_id = 10) LOOP
DBMS_OUTPUT.PUT_LINE(emp_rec.employee_id || ': ' || emp_rec.first_name);
ENDLOOP;
END;
/
Output
100: Steven
101: Neena
102: Lex
100: Steven
101: Neena
102: Lex
Prefer Implicit Cursor FOR Loops
A cursor FOR loop automatically opens, fetches, and closes the cursor. It's less code and eliminates the risk of forgetting to close the cursor, which can cause memory leaks.
Production Insight
Forgetting to CLOSE an explicit cursor in a long-running procedure caused a cursor-leak that exhausted the database's OPEN_CURSORS limit, crashing all new sessions.
The fix was to switch to implicit FOR loops, which handle lifecycle automatically.
Rule: use implicit FOR loops unless you need explicit control (e.g., BULK COLLECT, parameterized cursors).
Key Takeaway
For simple row-by-row processing, use a cursor FOR loop.
Always CLOSE explicit cursors in the same block where you opened them.
BULK COLLECT reduces SQL-to-PL/SQL context switches — use it for batch operations.
Stored Procedures and Functions
While anonymous blocks run once, stored procedures and functions live in the database and can be called repeatedly — by other PL/SQL, from application code, or even directly from SQL. Procedures perform actions (INSERT, UPDATE, etc.) and can have OUT parameters to return values. Functions return a single value and can be used in SQL statements if they are free of side effects (i.e., they don't modify database state). Packages group related procedures, functions, types, and variables together, providing encapsulation and namespace management.
procedures_functions.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
-- Stored procedure: updates salary and returns the new value via OUT parameterCREATEORREPLACEPROCEDUREraise_salary(
p_employee_id IN employees.employee_id%TYPE,
p_percent INNUMBER,
p_new_salary OUT employees.salary%TYPE
) ISBEGINUPDATE employees
SET salary = salary * (1 + p_percent/100)
WHERE employee_id = p_employee_id
RETURNING salary INTO p_new_salary;
COMMIT;
END raise_salary;
/
-- Stored function: returns a single value, callable from SQLCREATEORREPLACEFUNCTIONget_annual_salary(
p_employee_id employees.employee_id%TYPE
) RETURNNUMBERDETERMINISTIC-- Indicates same input always returns same outputIS
v_salary employees.salary%TYPE;
BEGINSELECT salary INTO v_salary
FROM employees WHERE employee_id = p_employee_id;
RETURN v_salary * 12;
EXCEPTIONWHEN NO_DATA_FOUND THENRETURNNULL;
END get_annual_salary;
/
-- Calling the function from SQLSELECT employee_id, get_annual_salary(employee_id) AS annual_salary
FROM employees
WHERE department_id = 10;
Output
100: 288000
101: 264000
102: 216000
Functions in SQL Must Be Pure
A function called in a SQL statement must not execute DML (INSERT, UPDATE, DELETE) or transaction statements (COMMIT, ROLLBACK). Doing so raises ORA-14551. Mark functions as DETERMINISTIC when appropriate to allow better performance through caching.
Production Insight
A function that modified a log table was called in an UPDATE statement — every row execution committed the log table, causing massive log file growth and performance degradation.
The fix: separate the logging into a procedure called after the UPDATE.
Rule: never write DML inside a function that will be called from SQL — use a procedure instead.
Functions used in SQL must be free of side effects.
Packages organise related PL/SQL units — use them for real applications.
● Production incidentPOST-MORTEMseverity: high
Unhandled TOO_MANY_ROWS Takes Down Nightly Batch
Symptom
Batch job exits with ORA-01422: exact fetch returns more than requested number of rows. No other output. The entire batch is rolled back.
Assumption
The developer assumed the SELECT INTO would always return at most one row because the WHERE clause used department_id and employee_id — but a foreign key constraint was missing.
Root cause
A data migration introduced duplicate employee_id values in a child table. The SELECT INTO used employee_id without DISTINCT or a guaranteed unique filter. With multiple rows, PL/SQL raised TOO_MANY_ROWS and the unhandled exception aborted the block.
Fix
Add an exception handler for TOO_MANY_ROWS with logging, then decide how to handle duplicates. Better: rewrite as a cursor loop or use an aggregation function like MAX() to guarantee a single row.
Key lesson
Always use exception handlers for NO_DATA_FOUND and TOO_MANY_ROWS when using SELECT INTO.
For production code, prefer explicit cursors with FOR loops — they handle zero, one, or many rows gracefully.
Test with edge-case data after any migration that affects lookup tables.
Production debug guideFollow these steps when a PL/SQL block fails silently or with an ORA- error.4 entries
Symptom · 01
PL/SQL block runs but produces no output
→
Fix
Run SET SERVEROUTPUT ON in your session before executing the block. Verify DBMS_OUTPUT is enabled via the client's output panel.
Symptom · 02
ORA-06512 (PL/SQL unhandled exception) with no line number
→
Fix
Enable full error backtrace: DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_BACKTRACE); in the EXCEPTION block. This shows the exact call stack.
Symptom · 03
ORA-01422 or ORA-01403 (wrong number of rows)
→
Fix
Check the SELECT INTO statement. Test with the same WHERE clause in a regular SQL query to see how many rows are returned. Use COUNT(*) to validate assumptions.
Symptom · 04
Block compiles but logic is wrong
→
Fix
Add debug variables: assign intermediate results and output them with DBMS_OUTPUT.PUT_LINE. Use IF ... THEN DBMS_OUTPUT.PUT_LINE('DEBUG: value=' || v_var); END IF;
★ Common PL/SQL Errors – Quick FixesWhen your PL/SQL block fails, here's the fastest path from symptom to resolution.
ORA-01403: No data found−
Immediate action
Check if the SELECT INTO query returns any rows. Run it as a standalone SQL.
Commands
SELECT COUNT(*) FROM employees WHERE department_id = 10;
ADD EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('No rows');
Fix now
Wrap the SELECT INTO in an exception handler and decide fallback logic.
ORA-01422: Exact fetch returns more than one row+
Immediate action
Run the SELECT as a plain SQL to see how many rows are returned.
Commands
SELECT employee_id, salary FROM employees WHERE department_id = 10;
Change SELECT INTO to a cursor FOR loop or use MAX(salary) to guarantee single row.
Fix now
Rewrite using an explicit cursor: CURSOR c IS ... ; FOR rec IN c LOOP ... END LOOP;
PL/SQL block does nothing (no output, no error)+
Immediate action
Check if SET SERVEROUTPUT ON is executed in the session.
Commands
SET SERVEROUTPUT ON;
Add a test output at the very beginning of BEGIN: DBMS_OUTPUT.PUT_LINE('Block started');
Fix now
Enable serveroutput and add a simple output to confirm execution.
SQL vs PL/SQL
Feature
SQL
PL/SQL
Type
Declarative
Procedural
Execution
One statement at a time
Blocks of logic
Conditionals
CASE (limited)
IF/ELSIF/ELSE, CASE
Loops
Not supported
FOR, WHILE, LOOP
Error handling
None
EXCEPTION block
Reusability
Views
Procedures, functions, packages
Key takeaways
1
PL/SQL extends SQL with variables, conditionals, loops, and exception handling
2
Every PL/SQL program is a block
DECLARE / BEGIN / EXCEPTION / END
3
%TYPE anchors variable types to table column definitions
use it always
4
PL/SQL runs inside the database engine
eliminates application-to-database round trips
5
Always handle NO_DATA_FOUND and TOO_MANY_ROWS when using SELECT INTO
6
Use implicit cursor FOR loops for simple multi-row processing
less code, less risk
7
Procedures for actions, functions for values
keep functions side-effect-free for SQL use
8
Never suppress errors with a blank WHEN OTHERS
always log and re-raise
Common mistakes to avoid
5 patterns
×
Forgetting SET SERVEROUTPUT ON
Symptom
DBMS_OUTPUT.PUT_LINE silently discards output. You see nothing and think the block didn't run.
Fix
Run SET SERVEROUTPUT ON at the start of your session. In SQL Developer, also ensure the Output pane is visible.
×
SELECT INTO when query returns multiple rows
Symptom
ORA-01422: Exact fetch returns more than requested number of rows. The block aborts.
Fix
Use a cursor with a FOR loop, or use an aggregate function (MAX, MIN) to guarantee a single row. Always consider edge cases where data might have duplicates.
×
Omitting the forward slash (/) in SQL*Plus
Symptom
The block is not executed. No error appears. The prompt just returns without any output.
Fix
Always end anonymous blocks with a slash on its own line. In SQL*Plus, that triggers execution.
×
Hardcoding column types instead of using %TYPE
Symptom
When the column definition changes (e.g., salary from NUMBER(8,2) to NUMBER(10,2)), variables may truncate values or raise numeric errors.
Fix
Always declare variables with table.column%TYPE or table%ROWTYPE.
×
Using WHEN OTHERS with just a comment or NULL
Symptom
Critical errors are silently swallowed. The procedure may appear to succeed but produces wrong data.
Fix
At minimum log the error with SQLERRM and DBMS_UTILITY.FORMAT_ERROR_BACKTRACE. Consider re-raising unless you have a specific reason to continue.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What are the four sections of a PL/SQL block, and which are optional?
Q02JUNIOR
What is the difference between %TYPE and %ROWTYPE?
Q03SENIOR
When would you use a PL/SQL procedure versus a function?
Q04SENIOR
Explain how exception propagation works in nested PL/SQL blocks.
Q05SENIOR
What are the performance implications of explicit vs implicit cursors?
Q01 of 05JUNIOR
What are the four sections of a PL/SQL block, and which are optional?
ANSWER
The sections are: DECLARE (optional, for variable declarations), BEGIN (required, contains executable statements), EXCEPTION (optional, handles errors), and END (required). The forward slash (/) after END is needed in command-line tools to execute the block.
Q02 of 05JUNIOR
What is the difference between %TYPE and %ROWTYPE?
ANSWER
%TYPE anchors a variable's type to a single column (e.g., employees.salary%TYPE). %ROWTYPE declares a record variable that matches all columns of a table or cursor (e.g., employees%ROWTYPE or cursor_name%ROWTYPE).
Q03 of 05SENIOR
When would you use a PL/SQL procedure versus a function?
ANSWER
Use a procedure when you need to perform actions (DML, multiple operations) or return multiple values via OUT parameters. Use a function when you need to compute and return a single value, especially if you intend to call it from a SQL statement. Functions in SQL must be deterministic and free of side effects.
Q04 of 05SENIOR
Explain how exception propagation works in nested PL/SQL blocks.
ANSWER
If an exception occurs in an inner block and is not handled, it propagates to the outer block's EXCEPTION section. If still unhandled, it propagates to the calling environment. Using BEGIN ... END with an inner EXCEPTION block allows you to catch and handle errors locally without affecting the outer logic. DBMS_UTILITY.FORMAT_ERROR_BACKTRACE can trace the exact propagation path.
Q05 of 05SENIOR
What are the performance implications of explicit vs implicit cursors?
ANSWER
For simple row-by-row processing, implicit cursor FOR loops are slightly faster because the Oracle engine optimises the open-fetch-close lifecycle. Explicit cursors offer more control (e.g., parameterised cursors, BULK COLLECT with LIMIT). For bulk operations, use BULK COLLECT with explicit cursors to minimize SQL-to-PL/SQL context switches.
01
What are the four sections of a PL/SQL block, and which are optional?
JUNIOR
02
What is the difference between %TYPE and %ROWTYPE?
JUNIOR
03
When would you use a PL/SQL procedure versus a function?
SENIOR
04
Explain how exception propagation works in nested PL/SQL blocks.
SENIOR
05
What are the performance implications of explicit vs implicit cursors?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between SQL and PL/SQL?
SQL is a declarative language for querying and manipulating data. PL/SQL is Oracle's procedural extension that adds programming constructs like loops, if-else, variables, and error handling. SQL executes one statement at a time; PL/SQL can execute blocks of logic as a single unit.
Was this helpful?
02
Do I need to compile PL/SQL before using it?
Anonymous blocks are interpreted on the fly — no separate compilation step. Stored procedures and functions are compiled once when created, and the compiled code is stored in the database for reuse. Compilation catches syntax errors; runtime errors need exception handling.
Was this helpful?
03
Can I use PL/SQL to modify data in another schema?
Yes, if the calling user has appropriate privileges on the target objects (e.g., INSERT or UPDATE). The privilege check happens at runtime. For stored procedures, you can use invoker's rights or definer's rights to control privilege model.
Was this helpful?
04
Why does my PL/SQL block not produce any output?
Most likely SET SERVEROUTPUT ON is missing. Without it, DBMS_OUTPUT.PUT_LINE output is discarded. In SQL Developer, check that the Output panel is visible and the serveroutput is enabled in the connection properties.
Was this helpful?
05
What is the purpose of the forward slash (/) at the end of a PL/SQL block?
In SQLPlus, SQLLoader, and some other Oracle tools, the slash on its own line tells the tool to execute the preceding PL/SQL block. Without it, the block is buffered but not run. Modern IDEs like SQL Developer often ignore the slash and execute automatically, but it's still standard practice.