PL/SQL stored procedures and functions are named, compiled subprograms that encapsulate SQL and PL/SQL logic inside the database
Procedures perform actions (DML, batch tasks) and return values via OUT parameters; Functions compute and return exactly one value via RETURN
Compiled once and cached in Oracle's library cache — every call skips parsing, giving consistent high-performance execution
Parameter modes: IN (default, read-only), OUT (write-back to caller), IN OUT (bidirectional) — each passed by reference
Never perform DML inside a function called from SQL — it violates purity level WNDS and throws ORA-14551
'WHEN OTHERS THEN NULL' is the most dangerous anti-pattern — it swallows errors and makes the application blind to failures
✦ Definition~90s read
What is PL/SQL Stored Procedures and Functions?
PL/SQL stored procedures and functions are named, server-side subprograms that live inside an Oracle database. They exist to move business logic closer to the data, reducing network round trips and centralizing rules that multiple applications must enforce.
★
Think of PL/SQL Stored Procedures and Functions as a powerful tool in your developer toolkit.
A procedure performs an action—inserting a row, updating inventory, calling an external API via UTL_HTTP—while a function returns a single value and is callable inside SQL statements like SELECT my_func(x) FROM dual. Both are compiled and stored in the database, giving you deterministic execution plans and transactional control that application-layer code cannot guarantee.
These subprograms are not just 'database functions' in the generic sense. They are first-class Oracle objects with explicit parameter modes (IN, OUT, IN OUT), autonomous transaction support, and access to the full PL/SQL engine including bulk collect, pipelined results, and fine-grained exception handling.
The distinction between procedure and function matters in production: misuse a function with side effects inside a SELECT and you'll corrupt data silently; forget an OUT parameter's behavior on exception and you'll lose $1.2M in a single batch job. The block structure—DECLARE, BEGIN, EXCEPTION, END—is mandatory, and every production subprogram must handle NO_DATA_FOUND, TOO_MANY_ROWS, and unhandled exceptions or you will leak cursors and roll back hours of work.
In the ecosystem, PL/SQL subprograms compete with Java stored procedures, Oracle's multitenant PL/SQL packages, and external procedures in C or Java. You should not use them for stateless REST endpoints (that's ORDS or APEX), for heavy computation that doesn't touch the database (that's application servers), or for logic that must scale horizontally without database licensing costs.
But for data-intensive operations—ETL transforms, financial calculations, real-time validation at insert time—they are unmatched in performance and atomicity. The gotcha is that silent exceptions in these subprograms are the #1 cause of data corruption in Oracle shops, because a failed procedure can leave partial commits, unclosed cursors, and no audit trail unless you explicitly log every exception path.
Plain-English First
Think of PL/SQL Stored Procedures and Functions as a powerful tool in your developer toolkit. Once you understand what it does and when to reach for it, everything clicks into place. Imagine you have a complex coffee machine. A 'Procedure' is like pressing the 'Make Latte' button: it performs a series of actions (grinds beans, steams milk, pours) to change the state of the world (gives you a coffee). A 'Function' is like the thermometer inside: you ask it for a value (the temperature), and it returns that specific piece of information to you so you can decide what to do next.
PL/SQL stored procedures and functions are schema objects that encapsulate SQL and PL/SQL statements into reusable, named units. By moving logic from the application tier to the database tier, you reduce network traffic, enforce security boundaries, and ensure complex business rules are applied consistently across all consuming services.
These subprograms solve two production problems simultaneously: repeated logic across applications and excessive network round-trips. Oracle parses, compiles, and optimizes the code once, storing the executable form in the library cache. Every application — Java microservice, Python script, or legacy ERP — benefits from the same execution plan.
By the end, you'll have the conceptual understanding and practical code examples to use PL/SQL stored procedures and functions with confidence, including the production failure modes that catch teams off guard.
Why PL/SQL Stored Procedures Are Not Just 'Database Functions'
A PL/SQL stored procedure is a named block of code stored and executed inside the Oracle database engine. Unlike ad-hoc SQL, it compiles once and runs in the database process space, eliminating network round-trips for each statement. The core mechanic: you pass parameters, the procedure executes a sequence of DML and procedural logic, and it either commits or rolls back as a unit — no partial writes.
Key properties that matter in production: stored procedures run with definer's rights by default, meaning they execute with the privileges of the procedure owner, not the caller. This is a double-edged sword — it simplifies permission management but can silently escalate privileges if misused. Also, PL/SQL uses implicit cursor for loops and bulk collect for array operations; forgetting bulk collect on a 10M-row table will cause a 10-minute pause while the client buffers every row.
Use stored procedures when you need transactional integrity across multiple tables, complex business rules that must execute close to the data, or when you want to hide table structure from client code. They are not a replacement for application logic — they are a tool for data integrity. In high-throughput systems, a well-tuned procedure can process 50,000 rows/second; a poorly written one with row-by-row processing will crater at 200 rows/second.
⚠ Silent Exception Trap
PL/SQL's default exception handler does nothing — if you don't add WHEN OTHERS THEN RAISE, the procedure completes 'successfully' while doing nothing.
📊 Production Insight
A batch job calling a stored procedure that silently caught a NO_DATA_FOUND exception — the procedure returned success, the job logged 'completed', and $1.2M in invoices were never generated.
Symptom: the procedure ran in 0.2 seconds instead of the usual 45 seconds, but no error was raised anywhere.
Rule: every stored procedure must have an explicit exception section that logs or re-raises — never rely on the default handler.
🎯 Key Takeaway
Stored procedures execute in the database engine — treat them as server-side transactions, not client-side functions.
Always handle exceptions explicitly; a silent catch is a production incident waiting to happen.
Use bulk operations (BULK COLLECT, FORALL) for any set larger than 100 rows — row-by-row is the #1 performance killer.
thecodeforge.io
Plsql Stored Procedures Functions
Visual Block Structure — Anatomy of a PL/SQL Subprogram
Every PL/SQL stored procedure or function follows a strict block structure that Oracle enforces at compile time. Understanding this structure is the foundation for writing correct, maintainable subprograms. The block consists of four sections: declarative (DECLARE), executable (BEGIN), exception (EXCEPTION), and the final END keyword. The declarative section is optional in anonymous blocks but mandatory for named subprograms — you must declare all variables, cursors, and constants before the executable section.
The executable section is the only mandatory part — it contains the procedural logic and SQL statements. The exception section is optional but highly recommended for production code. Even if you don't need custom error handling, including a basic WHEN OTHERS THEN RAISE ensures that unexpected errors are never silently swallowed. The block ends with END followed by the subprogram name (optional but good practice).
This structure is recursive: you can nest blocks inside the executable or exception sections of another block. Each nested block has its own DECLARE, BEGIN, EXCEPTION, and END. This is how you implement local subprograms inside a procedure — by placing a nested block with its own declarative section inside the parent executable section. The outermost block is the subprogram itself, and inner blocks provide scoped isolation for temporary variables and localized error handling.
io/thecodeforge/plsql/BlockStructure.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
-- io.thecodeforge: A procedure with nested local subprogramCREATEORREPLACEPROCEDURE io.thecodeforge.sp_process_order(
p_order_id INNUMBER
) IS-- Outer block declarative section
v_total NUMBER := 0;
v_line_count NUMBER := 0;
-- You can declare local functions/procedures here (used in 12c+)FUNCTIONget_tax(p_amount INNUMBER) RETURNNUMBERIS
v_tax NUMBER;
BEGIN
v_tax := p_amount * 0.08;
RETURN v_tax;
END get_tax;
BEGIN-- Outer block executable sectionSELECTSUM(line_amount) INTO v_total
FROM order_lines WHERE order_id = p_order_id;
v_line_count := SQL%ROWCOUNT;
-- Call a nested function
DBMS_OUTPUT.PUT_LINE('Tax: ' || get_tax(v_total));
EXCEPTION-- Outer block exception sectionWHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Order ' || p_order_id || ' has no lines');
WHENOTHERSTHENRAISE;
END sp_process_order;
/
Output
Procedure SP_PROCESS_ORDER compiled.
🔥Declaration Order Matters
PL/SQL requires forward declaration for local subprograms if they call each other. In the declarative section, you must declare a subprogram's signature before its body if another subprogram references it. Use the forward declaration pattern: declare the function header first, then its full body later.
📊 Production Insight
Nested blocks give you fine-grained exception control — you can catch and handle errors for a specific set of statements without affecting the rest of the procedure. This is critical for batch processing where one row failure shouldn't abort the entire batch.
The declarative section of a nested block isolates variables from the outer scope — no risk of variable name collisions or accidental overwrites.
🎯 Key Takeaway
PL/SQL block structure is recursive: outer block defines the subprogram, inner blocks provide scoped isolation and localized error handling. Always include at least a minimal exception section in production code.
PL/SQL Block Structure
Procedure vs Function Decision Matrix — When to Use Each
Decision Criteria
Procedure
Function
Need to perform DML (INSERT/UPDATE/DELETE)
Use procedure — side effects are expected and allowed
Use only if not called from SQL (pure logic pipeline)
Need to return a single value to caller
Use OUT parameter (multiple OUT parameters for multiple values)
Use RETURN clause — natural for single scalar result
Need to call from SQL (SELECT/WHERE)
Cannot be called from SQL
Can be called from SQL if pure (no DML, no transaction control)
Need to return multiple values
Use procedure with multiple OUT parameters
Cannot return multiple values — workaround: return a collection type
Batch processing with loops and logic
Use procedure — no purity restrictions
Use procedure or function if called from PL/SQL only (no SQL context)
Performance-sensitive read-only computation
Use DETERMINISTIC function with RESULT_CACHE hint — memoization built-in
Use function — can be cached by Oracle result cache
Need to overload based on parameter types
Supported in packages (overloading available)
Also support overloading in packages — same rules apply
Need to use in CHECK constraint
Cannot be used – side effects violate purity
Can be used if DETERMINISTIC and pure (no DML)
This matrix covers the primary decision factors. In practice, most production PL/SQL codebases use procedures for DML operations and functions for read-only business logic that will be used in SQL or as part of a calculation pipeline. When in doubt, lean toward a procedure for any subprogram that changes database state — it's safer and more explicit.
⚠ Function Purity Pitfall
A function called from SQL must not perform DML. Oracle enforces WNDS (Writes No Database State) at compile time. If you try to call a function that contains DML from a SELECT, you get ORA-14551. PRAGMA AUTONOMOUS_TRANSACTION does NOT satisfy WNDS — it bypasses the compile-time check but still causes a runtime error. The only workaround is to move the function logic to a procedure and call it separately.
📊 Production Insight
In production, the choice between procedure and function often comes down to where the logic will be called. If it's used inside SQL (reporting queries, data transformation), it must be a pure function. If it's orchestrated by application code or job schedulers, a procedure is more flexible and can include transaction control.
Overloading in packages gives you the best of both worlds — you can define both a procedure and a function version of the same logical operation, letting the caller choose the right signature.
🎯 Key Takeaway
Use a procedure for DML and batch operations; use a function for read-only computations callable from SQL. The decision matrix above covers the nine most common scenarios to guide your choice.
thecodeforge.io
Plsql Stored Procedures Functions
The Core Mechanics: Procedures for Action, Functions for Logic
PL/SQL Stored Procedures and Functions is a core feature of PL/SQL. It was designed to solve the problem of repeated logic and high network latency. Procedures are typically used to perform an action (DML operations like INSERT, UPDATE, or DELETE), while Functions are designed to compute and return a single value.
They exist to provide 'compiled' performance — Oracle parses, compiles, and optimizes the code once, storing the executable form in the database's library cache. This ensures that every application — whether it's a Java microservice, a Python script, or a legacy ERP — benefits from the same high-performance execution plan and centralized business logic. Furthermore, by granting EXECUTE privileges on a procedure rather than direct SELECT/UPDATE access on tables, you implement a robust 'Principle of Least Privilege' security model.
io/thecodeforge/plsql/ForgeSubprograms.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
39
40
41
-- io.thecodeforge: Function to calculate discounted price-- Optimized with DETERMINISTIC ifecodeforge.fn_get_discounted_price(
p_price INNUMBER,
p results are consistent for inputs
CREATEORREPLACEFUNCTION io.th_discount INNUMBER
) RETURNNUMBERDETERMINISTICISBEGIN-- Functions must return a value and avoid side effectsRETURNROUND(p_price * (1 - (p_discount / 100)), 2);
END fn_get_discounted_price;
/
-- io.thecodeforge: Procedure to update project budgetCREATEORREPLACEPROCEDURE io.thecodeforge.sp_update_budget(
p_project_id INNUMBER,
p_new_budget INNUMBER
) IS
v_old_budget NUMBER;
BEGIN-- Logical validation before DMLSELECT budget INTO v_old_budget FROM forge_projects WHERE id = p_project_id;
IF p_new_budget < 0THEN
RAISE_APPLICATION_ERROR(-20001, 'Budget cannot be negative.');
ENDIF;
UPDATE forge_projects
SET budget = p_new_budget,
last_updated = SYSDATEWHERE id = p_project_id;
-- Commit logic usually belongs to the caller, but can be here for batch tasksCOMMIT;
EXCEPTIONWHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20002, 'Project ID not found.');
WHENOTHERSTHENROLLBACK;
RAISE; -- Propagate error to the application tierEND sp_update_budget;
/
Output
Function FN_GET_DISCOUNTED_PRICE compiled.
Procedure SP_UPDATE_BUDGET compiled.
Mental Model
Procedures vs Functions Mental Model
Think of a procedure as a verb — it does something. Think of a function as a noun — it gives you something back.
Procedures: execute DML, run batch jobs, send notifications — they change the world
Functions: compute values, look up data, transform inputs — they answer questions
A procedure returns control to the caller; a function returns a value to the caller
Functions called from SQL must be pure (no DML) — procedures have no such restriction
Grant EXECUTE on subprograms instead of direct table access for least-privilege security
📊 Production Insight
Compiled subprograms skip SQL parsing on every call — library cache hits reduce CPU by 15-30% on hot paths.
Granting EXECUTE instead of direct table access implements row-level security without application changes.
Rule: if two applications run the same business logic, centralize it in a stored procedure — one source of truth.
🎯 Key Takeaway
Procedures are verbs — they perform actions and change database state.
Functions are nouns — they compute and return a single value, callable from SQL if pure.
Punchline: centralize shared business logic in the database as subprograms — one source of truth beats duplicated logic across five microservices.
Choosing Between Procedure and Function
IfNeed to perform INSERT, UPDATE, or DELETE
→
UseUse a procedure — functions should not have side effects
IfNeed to compute and return a single value for use in SQL
→
UseUse a function with DETERMINISTIC hint — callable from SELECT/WHERE
IfNeed to return multiple values from one call
→
UseUse a procedure with OUT parameters — functions return only one value
IfLogic is purely computational with no DML
→
UseUse a function — enables SQL-level usage and RESULT_CACHE optimization
Parameter Modes and Production Gotchas
When learning PL/SQL Stored Procedures and Functions, most developers hit the same set of gotchas. A frequent mistake is performing DML (Insert/Update/Delete) inside a Function; while technically possible using 'Autonomous Transactions,' it prevents the function from being used in a standard SELECT statement because it violates purity levels (WNDS - Writes No Database State).
Another error is failing to specify parameter modes (IN, OUT, IN OUT) correctly. By default, parameters are 'IN' (read-only). If you need to return multiple values from a single subprogram, a Procedure with 'OUT' parameters is your best friend. However, be cautious with 'IN OUT' parameters, as they can lead to side effects where the calling environment's variable is mutated unexpectedly. Production-grade code should also consider the 'NOCOPY' hint for large collection parameters to avoid expensive memory copying.
io/thecodeforge/plsql/CommonMistakes.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- io.thecodeforge: Using OUT parameters correctly for metadata retrievalCREATEORREPLACEPROCEDURE io.thecodeforge.sp_get_project_meta(
p_id INNUMBER,
p_name OUTVARCHAR2,
p_version OUTVARCHAR2,
p_status OUTVARCHAR2
) ISBEGIN-- One trip to the DB to fetch multiple values (Network Efficiency)SELECT name, version_info, status
INTO p_name, p_version, p_status
FROM forge_projects
WHERE id = p_id;
EXCEPTIONWHEN NO_DATA_FOUND THEN
p_name := 'NOT_FOUND';
p_version := '0.0.0';
p_status := 'INACTIVE';
WHEN TOO_MANY_ROWS THEN
RAISE_APPLICATION_ERROR(-20003, 'Data integrity error: duplicate project IDs.');
END sp_get_project_meta;
/
Output
// Procedure successfully returns 3 distinct values to the caller.
⚠ Watch Out: DML Inside Functions Called from SQL
A function called from a SELECT statement must not perform DML. Oracle enforces the WNDS (Writes No Database State) purity level. If you need to log or audit inside a function, use PRAGMA AUTONOMOUS_TRANSACTION — but understand it runs in a separate transaction and cannot see uncommitted changes from the caller.
📊 Production Insight
IN OUT parameters silently mutate the caller's variable — debugging side effects across call stacks is painful.
NOCOPY hint passes large collections by reference instead of copying — can reduce memory usage by 80% on big payloads.
Rule: use IN and OUT separately unless you truly need bidirectional mutation — IN OUT is a code smell in most cases.
🎯 Key Takeaway
IN is default and read-only — OUT writes back to the caller — IN OUT does both but causes surprise mutations.
NOCOPY on large collections avoids expensive pass-by-value memory copying.
Punchline: never use IN OUT unless you explicitly need bidirectional mutation — prefer separate IN and OUT parameters for clarity.
Choosing Parameter Modes
IfPassing data into the subprogram, no modification needed
→
UseUse IN (default) — read-only, safest mode
IfReturning a value to the caller
→
UseUse OUT — caller's variable is overwritten on return
IfReturning multiple values from one call
→
UseUse multiple OUT parameters in a procedure — functions only return one value
IfPassing a large PL/SQL collection (TABLE, VARRAY)
→
UseAdd NOCOPY hint to avoid expensive memory copy on each call
IfNeed to read AND modify the caller's variable
→
UseUse IN OUT — but document the mutation clearly, it causes surprise side effects
Exception Handling: The Production Safety Net
Exception handling in PL/SQL is not optional — it is the difference between a procedure that fails gracefully with a meaningful error and one that silently corrupts data or leaves the application blind. Oracle provides named exceptions (NO_DATA_FOUND, DUP_VAL_ON_INDEX, TOO_MANY_ROWS) for common conditions, and RAISE_APPLICATION_ERROR for custom business-rule violations.
The single most dangerous pattern in production PL/SQL is WHEN OTHERS THEN NULL. It catches every exception — including ones you never anticipated — and discards them silently. The procedure completes 'successfully,' the caller thinks the operation worked, but the data was never written. This pattern has caused silent data corruption in every Oracle shop I've worked in.
Production-grade exception handling follows three rules: (1) catch specific named exceptions first, (2) log unexpected errors before re-raising them, (3) never let an exception handler exit without either handling the error properly or propagating it to the caller.
io/thecodeforge/plsql/ExceptionHandling.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
39
40
41
42
43
44
45
46
47
48
49
-- io.thecodeforge: Production-grade exception handling patternCREATEORREPLACEPROCEDURE io.thecodeforge.sp_process_payment(
p_order_id INNUMBER,
p_amount INNUMBER,
p_status OUTVARCHAR2
) IS
v_customer_id NUMBER;
v_balance NUMBER;
BEGIN-- Validate order existsSELECT customer_id INTO v_customer_id
FROM forge_orders WHERE order_id = p_order_id;
-- Check balanceSELECT balance INTO v_balance
FROM forge_customers WHERE customer_id = v_customer_id;
IF v_balance < p_amount THEN
RAISE_APPLICATION_ERROR(-20100, 'Insufficient balance for order ' || p_order_id);
ENDIF;
-- Deduct and recordUPDATE forge_customers SET balance = balance - p_amount
WHERE customer_id = v_customer_id;
INSERTINTOforge_payments (order_id, amount, paid_on)
VALUES (p_order_id, p_amount, SYSDATE);
COMMIT;
p_status := 'SUCCESS';
EXCEPTIONWHEN NO_DATA_FOUND THENROLLBACK;
p_status := 'ORDER_NOT_FOUND';
WHEN DUP_VAL_ON_INDEX THENROLLBACK;
p_status := 'DUPLICATE_PAYMENT';
WHENOTHERSTHENROLLBACK;
-- Log the unexpected error for investigation
io.thecodeforge.logging.log_error(
p_proc => 'sp_process_payment',
p_errm => SQLERRM,
p_ctx => 'order_id=' || p_order_id
);
RAISE; -- Always re-raise unexpected errorsEND sp_process_payment;
/
Output
Procedure SP_PROCESS_PAYMENT compiled.
Mental Model
Exception Handling Mental Model
Think of exception handling like a fire alarm system: specific alarms (named exceptions) tell you exactly what went wrong. The general alarm (WHEN OTHERS) should trigger a full evacuation (log + re-raise), not be silenced.
Named exceptions first: catch NO_DATA_FOUND, DUP_VAL_ON_INDEX, TOO_MANY_ROWS explicitly
WHEN OTHERS is your last resort — always log and re-raise, never swallow
ROLLBACK before handling to leave the transaction in a clean state
Use RAISE_APPLICATION_ERROR with custom codes (-20000 to -20999) for business-rule violations
Pass context (order_id, customer_id) to the error log — generic 'error occurred' messages are useless
📊 Production Insight
WHEN OTHERS THEN NULL has caused silent data corruption in every Oracle shop I've worked in.
RAISE_APPLICATION_ERROR with custom error codes lets the application tier distinguish business errors from system errors.
Rule: every exception handler must either handle the error completely or re-raise it — silence is never an option.
🎯 Key Takeaway
WHEN OTHERS THEN NULL is the most dangerous anti-pattern — it makes every error invisible to the caller.
Always log unexpected errors with context before re-raising — generic 'error occurred' logs are useless for debugging.
Punchline: every exception handler must either handle the error completely or re-raise it — silence in production is never acceptable.
Exception Handling Strategy
IfExpected error with a clear recovery path (e.g., row not found)
→
UseCatch the named exception, handle gracefully, set OUT status
UseUse RAISE_APPLICATION_ERROR with a custom error code (-20000 to -20999)
IfUnexpected error you didn't anticipate
→
UseLog with context (SQLERRM, parameters), then RAISE to propagate to the caller
IfBatch job where one row failing should not stop the rest
→
UseCatch exceptions inside the loop, log the failure, continue to next row
Cursor Management — Explicit Control for Production Workloads
### Cursor Types Overview - Implicit Cursor: Used by SQL statements without a cursor name. Access via SQL% attributes (SQL%ROWCOUNT, SQL%FOUND, SQL%NOTFOUND, SQL%ISOPEN). Best for single-row operations (SELECT INTO) and DML where you only need to check row count. - Explicit Cursor: Declared in the declarative section with CURSOR name IS query. You control OPEN, FETCH, CLOSE. Use when you need to process rows one by one with conditional logic, or when you need to use BULK COLLECT. - Cursor FOR Loop: Automatically opens, fetches, and closes. The most readable pattern. Use when you need to iterate over every row sequentially without complex control logic. - REF CURSOR: A cursor variable (SYS_REFCURSOR) that can point to any query result. Used for dynamic SQL, passing result sets between subprograms, or returning result sets to client applications.
### Best Practices for Production 1. Always close explicit cursors — use a single CLOSE in the exception section to handle cleanup. 2. Use cursor FOR loops by default — they are less error-prone than explicit cursors. 3. Use BULK COLLECT for large result sets — reduces context switches between SQL and PL/SQL engines. 4. Use SELECT FOR UPDATE with explicit cursors when you need to lock rows before updating them — avoids race conditions. 5. For dynamic queries, use REF CURSOR with DBMS_SQL or native dynamic SQL (EXECUTE IMMEDIATE).
### Code Example: Explicit Cursor with Bulk Collect
io/thecodeforge/plsql/CursorManagement.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
-- io.thecodeforge: Explicit cursor with BULK COLLECT for batch processingDECLARETYPE order_rec_type ISRECORD (
order_id NUMBER,
total_amount NUMBER
);
TYPE order_tab_type ISTABLEOF order_rec_type;
l_orders order_tab_type;
CURSOR c_orders ISSELECT o.id, NVL(SUM(ol.line_amount), 0) as total
FROM orders o
LEFTJOIN order_lines ol ON ol.order_id = o.id
WHERE o.status = 'NEW'GROUPBY o.id;
BEGINOPEN c_orders;
LOOPFETCH c_orders BULKCOLLECTINTO l_orders LIMIT1000;
EXITWHEN l_orders.COUNT = 0;
FORALL i IN1..l_orders.COUNTUPDATE orders
SET total = l_orders(i).total_amount,
status = 'PROCESSED'WHERE id = l_orders(i).order_id;
COMMIT;
ENDLOOP;
CLOSE c_orders;
EXCEPTIONWHENOTHERSTHENIF c_orders%ISOPENTHENCLOSE c_orders;
ENDIF;
RAISE;
END;
/
Output
PL/SQL procedure successfully completed.
💡Cursor Attribute Quick Reference
Implicit cursor attributes: SQL%ROWCOUNT (number of rows affected), SQL%FOUND (true if at least one row), SQL%NOTFOUND (true if zero rows), SQL%ISOPEN (always false for implicit). Explicit cursor attributes: cursor_name%ROWCOUNT, cursor_name%FOUND, cursor_name%NOTFOUND, cursor_name%ISOPEN. After a BULK COLLECT, use cursor_name%ROWCOUNT to get the total number of rows fetched, or check the collection's COUNT.
📊 Production Insight
Explicit cursors with BULK COLLECT and LIMIT reduce context switches by 90% compared to row-by-row processing. For procedures processing more than 10,000 rows, always use this pattern.
REF CURSOR parameters allow you to build generic data export procedures — pass in any query and let the procedure iterate over the result set without knowing the structure.
Cursor FOR loops are safe and efficient for OLTP workloads with small result sets — they automatically bulk collect in 100-row batches in recent Oracle versions.
🎯 Key Takeaway
Explicit cursors give you fine-grained control but require manual lifecycle management. Use cursor FOR loops by default, explicit cursors with BULK COLLECT for batch processing, and REF CURSOR for dynamic result sets. Always close cursors in an exception handler.
Bulk vs Row — Performance Benchmarking for Batch Operations
Operation Type
Rows Processed
Row-by-Row Time
Bulk Time
Speedup
Simple UPDATE with static condition
10,000
3.2 seconds
0.12 sec
27x
INSERT from SELECT with aggregation
50,000
18.5 seconds
0.45 sec
41x
UPDATE with correlated subquery
100,000
45 seconds
0.9 sec
50x
DELETE with join (10 tables)
200,000
2.1 minutes
2.1 sec
60x
Complex INSERT with transformations
500,000
8.5 minutes
5.8 sec
88x
These benchmarks highlight that the performance gain is proportional to the number of rows and the complexity of the SQL. For small sets (<1,000 rows), the overhead of bulk operations may not be worth it — row-by-row is simpler and fast enough. But for any batch processing above 1,000 rows, bulk operations are mandatory in production.
The key technique is: use BULK COLLECT to fetch large result sets into a PL/SQL collection, then use FORALL to execute the same DML for each row in the collection. FORALL performs context switching only once per FORALL statement, rather than once per row. Additionally, use LIMIT clause in BULK COLLECT to control memory usage — 1,000 rows per fetch is a good starting point for most workloads.
io/thecodeforge/plsql/BulkVsRow.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
-- io.thecodeforge: Row-by-row vs Bulk comparison for archiving old orders-- ROW-BY-ROW APPROACH (slow)BEGINFOR r IN (SELECT * FROM orders WHERE created_date < SYSDATE - 365) LOOPINSERTINTO orders_archive VALUES r;
DELETEFROM orders WHERE id = r.id;
COMMIT;
ENDLOOP;
END;
/
-- BULK APPROACH (fast)DECLARETYPE order_tab ISTABLEOF orders%ROWTYPE;
l_orders order_tab;
BEGINSELECT * BULKCOLLECTINTO l_orders
FROM orders WHERE created_date < SYSDATE - 365;
FORALL i IN1..l_orders.COUNTINSERTINTO orders_archive VALUESl_orders(i);
FORALL i IN1..l_orders.COUNTDELETEFROM orders WHERE id = l_orders(i).id;
COMMIT;
END;
/
Output
PL/SQL procedure successfully completed. -- Bulk version completes in 1.2s vs 45s for row-by-row on 50k rows.
🔥When Not to Use Bulk Operations
Avoid BULK COLLECT when the result set is small (hundreds of rows) or when you need to process rows in a specific order with interdependent logic (e.g., update row 1, then row 2 based on row 1's new value). Bulk operations are not designed for sequential dependency within the same collection. For such cases, use row-by-row with a regular FOR loop or a cursor.
📊 Production Insight
In production, always start with a bulk approach for any batch procedure that processes more than 1,000 rows. The overhead of writing PL/SQL collections is negligible compared to the performance gain. Monitor memory consumption — for very large result sets (>1 million rows), use LIMIT in BULK COLLECT to avoid out-of-memory errors.
Oracle's FORALL with SAVE EXCEPTIONS allows you to continue processing even if some rows fail — critical for batch jobs where partial success is acceptable.
🎯 Key Takeaway
Bulk operations (BULK COLLECT + FORALL) achieve 27x to 88x speedup over row-by-row for batch DML. Use LIMIT to control memory usage. For small sets (<1,000 rows), row-by-row is simpler and adequate; above that, bulk is mandatory.
The RETURN Trap — Why Your Function’s Contract Is a Lie
Every PL/SQL function promises one thing: a single value back. New devs treat RETURN like a polite handshake. It’s not. It’s a hand grenade you pull the pin on.
RETURN immediately exits the function. No cleanup. No fall-through. No ‘I’ll just set the return value and keep going’. Any code after the first RETURN is dead code the compiler will warn you about. If you nest RETURN inside an IF-ELSIF chain without an ELSE... congrats. You’ve just introduced a NULL path that will silently blow up whoever calls your function with unexpected data.
Production gotcha: You write a function that returns a calculated tax amount. You protect the happy path, but forget RETURN in the ELSE. Oracle compiles it fine — because the function has at least one RETURN. But when the ELSE fires, the function returns NULL. Your calling procedure doesn’t check for NULL. Your invoice shows ‘Tax: ’. The customer calls support at 2 AM.
The fix? Make the compiler work for you. Declare a local variable, assign it in the body, and have exactly one RETURN at the end. Or use an exception handler that returns your fallback value. Do not scatter RETURN statements through conditional branches like confetti.
-- Test: SELECT calculate_tax(100, 'UK') FROM dual;
-- Result: 0 (not NULL)
⚠ Production Trap:
A function with multiple RETURN paths that misses one will return NULL silently. Always end with a single RETURN from a local variable, or wrap in an exception handler that returns a safe default.
🎯 Key Takeaway
One function, one exit. A single RETURN at the end keeps your contracts honest.
Built-in Functions — The Ones That Actually Prevent Incidents
Oracle ships over 200 built-in functions. Most are junk you’ll never touch. But five of them will save your bacon in production — not because they’re fancy, but because they kill edge cases before they turn into 3 AM pages.
First: NVL and COALESCE. They are not the same. NVL takes exactly two arguments. If the first is NULL, it returns the second. COALESCE takes N arguments and returns the first non-NULL. When you’re joining three tables and one column could be NULL from any of them, COALESCE handles it without nested NVL calls that look like Russian nesting dolls.
Second: TO_NUMBER with error handling. The default TO_NUMBER throws a hard error if the string isn’t clean. Pass '12A' and your batch job dies. Wrap it in a CASE or use the DEFAULT... ON CONVERSION ERROR clause (Oracle 12c+). Keeps bad data from halting your entire pipeline.
Third: TRUNC on dates. Everyone knows it. Nobody uses it correctly. TRUNC(SYSDATE) gives midnight. TRUNC(SYSDATE, 'MM') gives first of the month. If your WHERE clause compares SYSDATE to a column that has time components, you’re losing rows. TRUNC both sides or die by off-by-one bugs.
Fourth: LISTAGG with ON OVERFLOW TRUNCATE. Reports with concatenated data? Great until a row overflows 4000 chars. Then Oracle errors. Add 'ON OVERFLOW TRUNCATE' and your report survives production data.
Fifth: REGEXP_LIKE for input validation. You catch bad data at the database level instead of hoping the frontend is honest. '^[A-Z0-9]+$' on part numbers stops injection and garbage in one regex.
SafeBuiltins.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — database tutorial
-- Bad: batch dies on bad input-- SELECT TO_NUMBER('12A') FROM dual; -- ORA-01722-- Good: safe conversion with defaultSELECTCOALESCE(
TO_NUMBER('12A'DEFAULT0ONCONVERSIONERROR),
-1
) AS safe_part_number
FROM dual;
-- Date range trap:-- WHERE order_date BETWEEN SYSDATE AND SYSDATE+1 -- Wrong! includes time-- Fix:WHERETRUNC(order_date) BETWEENTRUNC(SYSDATE) ANDTRUNC(SYSDATE+1);
-- Overflow-proof concatenation:SELECTLISTAGG(description, ', 'ONOVERFLOWTRUNCATE'...')
FROM parts_table;
Output
SAFE_PART_NUMBER
---------------
0
-- The query runs, doesn't throw, returns a usable default.
🔥Senior Shortcut:
Use COALESCE for ANY nullable column chain. Use TO_NUMBER with DEFAULT ON CONVERSION ERROR instead of handling exceptions. Your DBA will thank you at 2 AM.
🎯 Key Takeaway
Three built-in functions — NVL/COALESCE, TRUNC, and safe TO_NUMBER — eliminate 80% of NULL and type-conversion incidents in production PL/SQL.
Terminologies in PL/SQL Subprograms — Precision Over Confusion
In production, mislabeling subprogram components causes silent logic failures. A subprogram is the generic term for any procedure or function defined in a PL/SQL block. The specification (or header) declares the name, parameters, and return type (for functions)—this is the contract. The body holds the executable statements and optional exception handlers. A parameter is a variable in the spec that receives or returns values; its mode (IN, OUT, IN OUT) determines data flow. Local variables exist only in the body’s declarative section. Formal parameters are the names in the spec; actual parameters are the values passed at call time. Mismatch between these terms in code reviews leads to hard-to-debug runtime errors. Use distinct naming conventions—prefix formal parameters with p_ and locals with l_—to enforce clarity. When debugging production incidents, reading error stacks requires knowing exactly which subprogram part failed. Get the terms right, and you read stack traces like a map, not a riddle.
SubprogramTerminology.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — database tutorial
CREATEORREPLACEPROCEDUREvalidate_order(
p_order_id IN orders.id%TYPE, -- formal parameter
p_status OUTVARCHAR2-- formal parameter with mode
) IS
l_count NUMBER := 0; -- local variableBEGINSELECTCOUNT(*)
INTO l_count
FROM orders
WHERE id = p_order_id; -- actual parameter in useIF l_count = 0THEN
p_status := 'NOT_FOUND';
ELSE
p_status := 'VALID';
ENDIF;
END;
Output
PL/SQL procedure successfully created.
⚠ Production Trap:
Never shadow a formal parameter name with a local variable. It silently uses the local, breaking the OUT assignment.
🎯 Key Takeaway
Formal parameters define the interface; local variables live only in the body. Never reuse names between them.
Built-in Functions in PL/SQL — Production Shortcuts That Prevent Incidents
PL/SQL built-in functions are not academic—they replace custom code that introduces bugs. Use NVL, NVL2, or COALESCE to handle nulls before they crash calculations. LEAST and GREATEST avoid multi-line IF blocks for min/max comparisons. SUBSTR, INSTR, and TRIM prevent string truncation errors in ETL pipelines. The conversion trio—TO_CHAR, TO_NUMBER, TO_DATE—must include explicit format masks to avoid locale-driven failures. For aggregates, use LNNVL to handle nulls in WHERE clauses without rewriting logic. For debugging, SQLCODE and SQLERRM in exception blocks give exact error numbers, not vague messages. DATE arithmetic is built-in: SYSDATE + 7 is valid, no function needed. The trap: assuming a built-in does what a custom function does. Always verify null behavior in your version. Write wrappers only when a built-in lacks an edge case your data demands. Otherwise, trust the built-in—it’s already tested under production load.
TO_CHAR without a format mask can return different date strings across DB character sets—always specify the mask.
🎯 Key Takeaway
Built-in functions eliminate null-handling bugs and locale surprises. Use explicit format masks on all conversions.
● Production incidentPOST-MORTEMseverity: high
WHEN OTHERS THEN NULL swallows constraint violation — silent data corruption for 14 hours
Symptom
Nightly reconciliation showed $1.2M in payments recorded in the application ledger but missing from the Oracle payment table. No errors in application logs. No alerts fired. The procedure completed successfully every time.
Assumption
The team assumed a network issue between the application and database, or a message queue dropping events.
Root cause
A developer added WHEN OTHERS THEN NULL to the procedure's exception block to 'prevent crashes.' When a foreign key constraint was violated (referencing a deleted customer ID), the exception was caught and silently discarded. The COMMIT still executed, but the INSERT never happened. The application received a success response because the procedure never raised an error.
Fix
Replaced WHEN OTHERS THEN NULL with explicit exception handlers for each expected error (DUP_VAL_ON_INDEX, NO_DATA_FOUND) and a final WHEN OTHERS THEN that calls a logging procedure and re-raises with RAISE. Added a post-batch verification step that compares application-side counts with database-side counts.
Key lesson
WHEN OTHERS THEN NULL is never acceptable in production — it makes every error invisible
Always re-raise unexpected exceptions after logging: WHEN OTHERS THEN log_error(SQLERRM); RAISE;
Batch procedures that claim success must be verifiable — add row-count assertions after DML blocks
Code review must flag any exception handler that does not re-raise or explicitly handle the error
Production debug guideCommon PL/SQL procedure and function failures and how to diagnose them6 entries
Symptom · 01
Procedure compiles but status shows INVALID in USER_OBJECTS
→
Fix
A dependency changed. Run: SELECT name, type, referenced_name, referenced_type FROM USER_DEPENDENCIES WHERE name = 'YOUR_PROC'. Recompile with ALTER PROCEDURE name COMPILE;
Symptom · 02
ORA-14551: cannot perform a DML operation inside a query
→
Fix
A function called from SELECT contains INSERT/UPDATE/DELETE. Remove DML from the function or wrap it in PRAGMA AUTONOMOUS_TRANSACTION (with caution).
Symptom · 03
Procedure runs but no rows are updated — zero rows affected
→
Fix
Check WHERE clause parameters. Run the SELECT with the same parameters first. Common cause: parameter name shadows a column name due to missing p_ prefix convention.
Symptom · 04
Application receives ORA-06502: PL/SQL numeric or value error
→
Fix
A variable is too small for the data. Check VARCHAR2 length and NUMBER precision. Query USER_SOURCE to find the line, then increase the variable size.
Symptom · 05
Function returns NULL unexpectedly
→
Fix
Check for NULL propagation in expressions. In PL/SQL, any expression with a NULL operand returns NULL. Use NVL() or COALESCE() to provide defaults.
Symptom · 06
Slow performance — procedure takes 30+ seconds
→
Fix
Enable SQL tracing: ALTER SESSION SET SQL_TRACE = TRUE; Run the procedure. Analyze the trace file with TKPROF. Look for full table scans and missing indexes.
Stored Procedure vs Function in PL/SQL
Feature
Stored Procedure
Function
Return Value
Does not return a value via header; uses OUT parameters if needed.
Must return exactly one value via the RETURN clause.
Call in SQL
Cannot be called in a SELECT or WHERE clause.
Can be called in SELECT/WHERE if it is 'pure' (no DML).
DML Operations
Standard use case; expected to change database state.
Restricted; if called from SQL, it cannot perform DML.
Primary Purpose
Executing business tasks, batch processing, and DML.
Computing data, transformations, and returning specific values.
Parameter Modes
Supports IN, OUT, and IN OUT modes.
Supports IN (standard), OUT/IN OUT (rarely used/discouraged).
⚙ Quick Reference
10 commands from this guide
File
Command / Code
Purpose
iothecodeforgeplsqlBlockStructure.sql
CREATE OR REPLACE PROCEDURE io.thecodeforge.sp_process_order(
Visual Block Structure
iothecodeforgeplsqlForgeSubprograms.sql
p_price IN NUMBER,
The Core Mechanics
iothecodeforgeplsqlCommonMistakes.sql
CREATE OR REPLACE PROCEDURE io.thecodeforge.sp_get_project_meta(
Parameter Modes and Production Gotchas
iothecodeforgeplsqlExceptionHandling.sql
CREATE OR REPLACE PROCEDURE io.thecodeforge.sp_process_payment(
Exception Handling
iothecodeforgeplsqlCursorManagement.sql
DECLARE
Cursor Management
iothecodeforgeplsqlBulkVsRow.sql
BEGIN
Bulk vs Row
ReturnGuard.sql
CREATE OR REPLACE FUNCTION calculate_tax(
The RETURN Trap
SafeBuiltins.sql
SELECT
Built-in Functions
SubprogramTerminology.sql
CREATE OR REPLACE PROCEDURE validate_order(
Terminologies in PL/SQL Subprograms
BuiltInFunctions.sql
DECLARE
Built-in Functions in PL/SQL
Key takeaways
1
PL/SQL Stored Procedures and Functions is a core concept in PL/SQL that every Database developer should understand to build modular, secure, and fast applications.
2
Procedures are verbs (actions), and Functions are nouns (values). Choose based on whether your primary goal is to change data or calculate it.
3
Encapsulating logic in the database creates a 'Single Source of Truth' for business rules, preventing different apps from calculating the same data differently.
4
Always use explicit exception handling to ensure the integrity of your DML and to provide meaningful feedback to the middleware tier.
5
Read the official documentation
it contains edge cases like 'Result Cache' hints which can drastically speed up functions that are called frequently with the same parameters.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the fundamental difference between a Procedure and a Function? W...
Q02JUNIOR
Explain the three parameter modes: IN, OUT, and IN OUT. Which is the def...
Q03SENIOR
What is 'RAISE_APPLICATION_ERROR' and why is it better than just letting...
Q04SENIOR
How do Stored Procedures improve performance for an application communic...
Q05SENIOR
Can a PL/SQL Function be overloaded? If so, what are the requirements re...
Q06SENIOR
What is the 'PRAGMA AUTONOMOUS_TRANSACTION' and how would you use it ins...
Q01 of 06JUNIOR
What is the fundamental difference between a Procedure and a Function? When is it illegal to use a Function in a SELECT statement?
ANSWER
A Procedure performs actions and does not return a value via its header (though it can use OUT parameters). A Function must return exactly one value via the RETURN clause. A Function cannot be used in a SELECT statement if it performs DML (INSERT/UPDATE/DELETE) — Oracle enforces the WNDS (Writes No Database State) purity level for functions called from SQL. Using PRAGMA AUTONOMOUS_TRANSACTION can bypass this, but the DML runs in a separate transaction.
Q02 of 06JUNIOR
Explain the three parameter modes: IN, OUT, and IN OUT. Which is the default, and how is it passed (by value or reference)?
ANSWER
IN is the default mode — the parameter is read-only inside the subprogram. OUT writes a value back to the caller's variable on return. IN OUT allows both reading and writing. By default, all modes are passed by value (copied). For large collections, use the NOCOPY hint to pass by reference and avoid expensive memory copying. IN OUT parameters can cause surprise side effects because the caller's variable is mutated.
Q03 of 06SENIOR
What is 'RAISE_APPLICATION_ERROR' and why is it better than just letting Oracle throw a generic system error?
ANSWER
RAISE_APPLICATION_ERROR lets you define custom error messages and error codes in the range -20000 to -20999. This is better than generic system errors because: (1) the application can distinguish business-rule violations from system errors by error code, (2) the error message is human-readable and context-specific, and (3) you can include dynamic context like 'Insufficient balance for order 12345' instead of a cryptic ORA-xxxxx.
Q04 of 06SENIOR
How do Stored Procedures improve performance for an application communicating over a network? Mention 'Context Switching'.
ANSWER
Stored procedures reduce network round-trips by executing multiple SQL statements in a single database call instead of one call per statement. They also eliminate SQL parsing overhead — the procedure is compiled once and cached in Oracle's library cache. Context switching between SQL and PL/SQL engines has a cost; batching SQL inside a PL/SQL FORALL or BULK COLLECT minimizes these switches and can improve throughput by 10-100x for batch operations.
Q05 of 06SENIOR
Can a PL/SQL Function be overloaded? If so, what are the requirements regarding parameter signatures?
ANSWER
Yes, PL/SQL supports overloading — multiple subprograms with the same name but different parameter signatures. The requirements: (1) the number of parameters must differ, or (2) the data types of corresponding parameters must differ, or (3) the mode (IN/OUT/IN OUT) must differ. You cannot overload based on return type alone. Overloading is commonly used in packages where you want the same logical operation to accept different input formats.
Q06 of 06SENIOR
What is the 'PRAGMA AUTONOMOUS_TRANSACTION' and how would you use it inside a function to log errors without rolling back the main transaction?
ANSWER
PRAGMA AUTONOMOUS_TRANSACTION declares a subprogram that runs in its own independent transaction. It can COMMIT or ROLLBACK without affecting the caller's transaction. Use case: an error logging function that must write to an audit table even if the main transaction fails and rolls back. The function declares the pragma, inserts the error record, commits, and returns — the main transaction's rollback won't undo the log entry. Caution: the autonomous transaction cannot see uncommitted data from the caller.
01
What is the fundamental difference between a Procedure and a Function? When is it illegal to use a Function in a SELECT statement?
JUNIOR
02
Explain the three parameter modes: IN, OUT, and IN OUT. Which is the default, and how is it passed (by value or reference)?
JUNIOR
03
What is 'RAISE_APPLICATION_ERROR' and why is it better than just letting Oracle throw a generic system error?
SENIOR
04
How do Stored Procedures improve performance for an application communicating over a network? Mention 'Context Switching'.
SENIOR
05
Can a PL/SQL Function be overloaded? If so, what are the requirements regarding parameter signatures?
SENIOR
06
What is the 'PRAGMA AUTONOMOUS_TRANSACTION' and how would you use it inside a function to log errors without rolling back the main transaction?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Can a stored procedure call a function?
Yes. A procedure can call a function anywhere an expression is allowed — in variable assignments, IF conditions, and SQL statements inside the procedure. The function must be compiled and the caller must have EXECUTE privilege on it.
Was this helpful?
02
What happens if a procedure with OUT parameters raises an exception before assigning values?
The OUT parameters retain their pre-call values (typically NULL unless the caller initialized them). The caller should always check SQLCODE or catch exceptions after the call rather than assuming OUT parameters were populated.
Was this helpful?
03
Can I use COMMIT inside a function?
Yes, but only if the function is not called from SQL (SELECT/WHERE). If called from SQL and it contains COMMIT, you must use PRAGMA AUTONOMOUS_TRANSACTION. Best practice: avoid COMMIT inside functions — let the caller control transaction boundaries.
Was this helpful?
04
What is the RESULT_CACHE hint and when should I use it?
RESULT_CACHE tells Oracle to cache the function's return value for a given set of input parameters. Subsequent calls with the same inputs return the cached result without re-executing. Use it for expensive, frequently-called functions where the inputs repeat. It is invalidated automatically when underlying tables are modified.