PL/SQL Complete Guide: Oracle Procedural Programming
SQL answers what data you want. PL/SQL answers how to process it. This complete guide takes you from your first PL/SQL block to production-grade packages, triggers, bulk processing, and dynamic SQL — with Oracle Retail context throughout.
What is PL/SQL & How It Differs from SQL
PL/SQL (Procedural Language/SQL) is Oracle's procedural extension of SQL. While SQL is a declarative language (you describe what you want), PL/SQL is procedural (you describe how to process data step by step).
PL/SQL runs directly inside the Oracle database engine, giving it three major advantages over application-side processing:
Security — business logic lives in the database, not in application code that can be bypassed or reverse-engineered.
Performance — SQL statements inside a PL/SQL block are executed in the same engine, eliminating the network round-trip overhead of sending each SQL statement from an application server.
Context — PL/SQL code has direct access to every Oracle feature: transactions, sequences, LOBs, UTL packages, Oracle Advanced Queuing, and more.
| Column | Type | Description |
|---|---|---|
Declarative | Style | You describe the result. The engine decides how to get it. |
Single statement | Unit | One complete query at a time. |
No variables | State | No way to store intermediate values. |
No conditional logic | Flow | Can't branch based on data values within the statement. |
No error handling | Errors | If a statement fails, it fails. |
PL/SQL adds all of this: variables, conditional logic (IF/CASE), loops, exception handling, cursors, and the ability to group reusable logic into stored procedures, functions, packages, and triggers.
SQL inside PL/SQL still uses the SQL engine
When you write a SELECT or INSERT inside a PL/SQL block, it is still executed by Oracle's SQL engine. PL/SQL orchestrates the flow; the SQL engine does the data work. This means SQL performance tuning (indexes, execution plans, bind variables) still applies inside PL/SQL.
PL/SQL Block Structure
Every PL/SQL program — from a two-line anonymous block to a 1000-line package body — follows the same four-section structure.
| Column | Type | Description |
|---|---|---|
DECLARE | Optional | Define all variables, cursors, constants, types, and exceptions used in the block. |
BEGIN | Mandatory | The executable section. Contains SQL statements and PL/SQL logic. |
EXCEPTION | Optional | Error handlers. Control returns here when a runtime error occurs. |
END; | Mandatory | Terminates the block. The trailing / executes the block in interactive tools. |
Anonymous blocks vs Named blocks
An anonymous block (no name, shown above) runs once and is not stored in the database. A named block — a procedure, function, package, or trigger — is compiled and stored in the data dictionary. Use anonymous blocks for one-off scripts; use named blocks for reusable logic.
Variables & Data Types
Variables store data temporarily during a PL/SQL block's execution. Every variable must have a data type.
Declaring Variables
Collections — PL/SQL-Specific Types
Always use %TYPE and %ROWTYPE in production code
Hardcoding VARCHAR2(50) for a variable that mirrors a column creates a maintenance trap — when the column is resized, your code silently breaks with data-truncation errors. v_name employees.first_name%TYPE adapts automatically and communicates intent clearly.
Conditional Statements — IF, ELSIF, CASE
IF / ELSIF / ELSE
Use ELSIF — not ELSEIF
Oracle PL/SQL uses ELSIF (no second E). Writing ELSEIF causes a compilation error. This trips up developers coming from MySQL (which uses ELSEIF) and most other languages (which use else if or elif).
CASE Statement in PL/SQL
Loops — LOOP, WHILE LOOP, FOR LOOP
PL/SQL provides three loop constructs. Choose based on whether you know the count upfront.
Basic LOOP — Run Until EXIT WHEN
WHILE LOOP — Condition-First
FOR LOOP — Known Range
Cursor FOR LOOP — Most Common in Production
Cursors — Implicit & Explicit
A cursor is a pointer to the memory area (cursor cache) where Oracle holds the result set of a SQL statement.
Implicit Cursors
Oracle automatically creates an implicit cursor for every DML statement (INSERT, UPDATE, DELETE) and SELECT INTO. You access its state via cursor attributes.
| Column | Type | Description |
|---|---|---|
SQL%FOUND | BOOLEAN | TRUE if the last DML/SELECT INTO affected/returned at least one row. |
SQL%NOTFOUND | BOOLEAN | TRUE if the last DML/SELECT INTO found no rows. |
SQL%ROWCOUNT | NUMBER | Number of rows affected by the last DML statement. |
SQL%ISOPEN | BOOLEAN | Always FALSE for implicit cursors (Oracle manages open/close automatically). |
Explicit Cursors
Defined by the developer when a SELECT can return multiple rows and you need row-by-row control.
Parameterized Cursor
Use Cursor FOR LOOP when you can
The Cursor FOR LOOP (FOR rec IN cursor_or_query LOOP) automatically handles OPEN, FETCH, %NOTFOUND check, and CLOSE. It is more concise and Oracle's optimizer can apply additional optimizations. Use explicit cursor control (OPEN/FETCH/CLOSE) only when you need to test %FOUND before the first fetch or share a cursor across multiple loops.
Exception Handling — Predefined & User-Defined
When a runtime error occurs in PL/SQL, execution stops and control passes to the EXCEPTION block. If no handler matches, the exception propagates to the calling block.
Predefined Exceptions
Oracle automatically raises these when specific error conditions occur.
| Column | Type | Description |
|---|---|---|
NO_DATA_FOUND | ORA-01403 | SELECT INTO returned zero rows. |
TOO_MANY_ROWS | ORA-01422 | SELECT INTO returned more than one row. |
ZERO_DIVIDE | ORA-01476 | Division by zero. |
DUP_VAL_ON_INDEX | ORA-00001 | INSERT/UPDATE violated a unique constraint. |
VALUE_ERROR | ORA-06502 | Type conversion error or string too long for variable. |
INVALID_CURSOR | ORA-01001 | Cursor operation on a cursor that is not open. |
OTHERS | Catch-all | Catches any exception not explicitly named above it. |
User-Defined Exceptions
RAISE_APPLICATION_ERROR — Returning Custom ORA Errors
WHEN OTHERS should always re-raise or log — never swallow silently
A bare WHEN OTHERS THEN NULL; swallows errors silently. This is one of the most dangerous patterns in Oracle development. Always at minimum log SQLCODE and SQLERRM. If you cannot handle the error, re-raise it: WHEN OTHERS THEN ROLLBACK; RAISE;
Procedures vs Functions
Procedures and functions are named PL/SQL blocks stored in the database. The key difference: a function must return a value; a procedure may or may not (via OUT parameters).
Stored Procedure
Stored Function
| Column | Type | Description |
|---|---|---|
Returns a value | Comparison | Optional (via OUT parameters). Mandatory via RETURN clause. |
Callable in SQL | Comparison | No (only from PL/SQL). Yes — can appear in SELECT, WHERE, ORDER BY. |
DML inside | Comparison | Yes. Only if function is not used in SQL DML context. |
Use case | Comparison | Actions, batch processing, complex logic. Calculations, transformations, lookups. |
Packages — Specification & Body
A package groups related procedures, functions, variables, and cursors into a single named unit. It is the primary way to organize PL/SQL code in enterprise Oracle systems.
Packages have two parts:
- Specification — the public interface (what callers can see and use)
- Body — the implementation (the actual code, invisible to callers)
Package Specification
Package Body
Package state is session-scoped
Package-level variables (like g_last_updated above) retain their values for the entire session. This is useful for caching, but can cause subtle bugs in connection-pooled environments where sessions are reused. Always initialize package state explicitly if relying on it.
Triggers — BEFORE, AFTER, INSTEAD OF
A trigger is a named PL/SQL block that fires automatically in response to a DML event (INSERT, UPDATE, DELETE) on a table or view, or a DDL/database event.
BEFORE Trigger — Validate or Modify Before the Change
AFTER Trigger — Audit Log After the Change
INSTEAD OF Trigger — DML on Views
| Column | Type | Description |
|---|---|---|
BEFORE | DML trigger | Before the DML executes. Both :OLD and :NEW available. |
AFTER | DML trigger | After the DML executes. Both :OLD and :NEW available. |
INSTEAD OF | View trigger | Replaces the DML entirely. Both :OLD and :NEW available. |
Avoid DML inside row-level triggers on the same table
A row-level trigger that does a SELECT or DML against its own triggering table will cause a mutating table error (ORA-04091). Use statement-level triggers, package-level collections, or compound triggers (Oracle 11g+) to work around this constraint.
BULK COLLECT & FORALL — Performance Processing
Every time PL/SQL communicates with the SQL engine it performs a context switch. When processing thousands of rows one at a time in a loop, thousands of context switches degrade performance dramatically. BULK COLLECT and FORALL solve this by processing data in sets.
BULK COLLECT — Fetch All Rows at Once
BULK COLLECT with LIMIT — Batch Processing for Large Tables
FORALL — Bulk DML
BULK COLLECT + FORALL is the standard pattern for high-volume processing
In Oracle Retail batch programs (like STKLEDGR, SAEXPAND, POSUPC), the BULK COLLECT + FORALL pattern is used throughout for processing millions of stock ledger rows, order lines, and price changes. A loop that processed 500,000 rows in 10 minutes with row-by-row cursors may run in under 30 seconds with bulk processing.
Dynamic SQL — EXECUTE IMMEDIATE
Dynamic SQL allows you to build and execute SQL or PL/SQL statements at runtime — when the full text of the statement is not known at compile time.
When you need Dynamic SQL:
- The table name changes at runtime (multi-tenant architectures)
- The WHERE clause structure varies based on user input
- DDL statements (CREATE TABLE, DROP INDEX) inside PL/SQL
- Running SQL across a list of table names from a metadata query
EXECUTE IMMEDIATE — DDL
EXECUTE IMMEDIATE — DML with Bind Variables
EXECUTE IMMEDIATE — Dynamic SELECT INTO
Building Dynamic WHERE Clauses
Always use bind variables — never concatenate user input into dynamic SQL
Concatenating user-provided values directly into SQL strings creates SQL injection vulnerabilities: 'SELECT ... WHERE name = ''' || p_user_input || ''''. Always use bind variables (USING p_value). For table and column names that must be dynamic, validate with DBMS_ASSERT.SQL_OBJECT_NAME() before concatenation.
Common Gotchas
Important Gotchas
- !
SELECT INTO with no rows raises NO_DATA_FOUND, not a warning. Unlike SQL where a no-result query returns an empty result set, SELECT INTO in PL/SQL raises a hard exception when no row is found. Always handle NO_DATA_FOUND explicitly or use a cursor FOR loop for zero-or-many row scenarios.
- !
SELECT INTO with multiple rows raises TOO_MANY_ROWS.
SELECT ... INTO v_var FROM table WHERE ...must return exactly one row. If your WHERE clause can match more than one row, use an explicit cursor or aggregate the result first. - !
WHEN OTHERS masking real errors. A WHEN OTHERS handler that only does
NULLswallows the error silently. The calling code has no idea something failed. Always log at minimumSQLCODEandSQLERRM, and usually ROLLBACK and re-raise. - !
Forgetting to CLOSE cursors. Unclosed cursors are not released until the session ends. In long-running batch jobs or connection-pooled environments, cursor leaks exhaust Oracle's
OPEN_CURSORSlimit, causing ORA-01000 errors. Cursor FOR loops close automatically — explicit OPEN/FETCH/CLOSE loops must call CLOSE. - !
Using ELSIF not ELSEIF. Oracle PL/SQL uses ELSIF (one word, no second E). Writing ELSEIF causes a compilation error. This catches developers coming from MySQL, Python, or most other languages.
- !
Dynamic SQL without bind variables. Concatenating values into SQL strings in EXECUTE IMMEDIATE creates SQL injection risk and pollutes the shared SQL area with non-reusable cursors (hard parses). Always use USING clause bind variables for values; validate object names with DBMS_ASSERT.
- !
Row-by-row cursor loops on large datasets. Fetching and processing 100,000 rows one at a time with 100,000 context switches is orders of magnitude slower than BULK COLLECT (one context switch) + FORALL. For any loop processing more than a few hundred rows, use bulk processing.
Key Takeaways
Key Takeaways
- Every PL/SQL program uses the four-section block structure: DECLARE (optional) → BEGIN (mandatory) → EXCEPTION (optional) → END. Anonymous blocks run once; named blocks (procedures, functions, packages, triggers) are stored in the database.
- Use %TYPE and %ROWTYPE to anchor variable declarations to column and table definitions. This prevents type mismatch bugs and communicates intent clearly.
- SELECT INTO must return exactly one row. Handle NO_DATA_FOUND and TOO_MANY_ROWS explicitly. For zero-or-many rows, use explicit cursors or Cursor FOR loops.
- Always CLOSE explicit cursors. Cursor FOR loops close automatically; OPEN/FETCH/CLOSE patterns require an explicit CLOSE call. Unclosed cursors cause ORA-01000 in production.
- Exception handling must not swallow errors silently. Log SQLCODE and SQLERRM in WHEN OTHERS. Use RAISE_APPLICATION_ERROR(-20000 to -20999) for business rule violations.
- Packages are the preferred way to organize production PL/SQL. The spec defines the public interface; the body contains the implementation. Package state is session-scoped.
- BULK COLLECT fetches multiple rows in one context switch. FORALL executes DML against a collection in one context switch. Together they reduce processing time for large datasets by orders of magnitude.
- EXECUTE IMMEDIATE allows runtime construction of SQL and DDL. Always use bind variables (USING clause) for values to prevent SQL injection and improve cursor reuse.


