Skip to content
Home Database PL/SQL Stored Procedures and Functions

PL/SQL Stored Procedures and Functions

Where developers are forged. · Structured learning · Free forever.
📍 Part of: PL/SQL → Topic 5 of 27
A comprehensive guide to PL/SQL Stored Procedures and Functions — master modular database programming, parameter modes, and return types in Oracle.
⚙️ Intermediate — basic Database knowledge assumed
In this tutorial, you'll learn
A comprehensive guide to PL/SQL Stored Procedures and Functions — master modular database programming, parameter modes, and return types in Oracle.
  • 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.
  • Procedures are verbs (actions), and Functions are nouns (values). Choose based on whether your primary goal is to change data or calculate it.
  • Encapsulating logic in the database creates a 'Single Source of Truth' for business rules, preventing different apps from calculating the same data differently.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • 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
Production IncidentWHEN OTHERS THEN NULL swallows constraint violation — silent data corruption for 14 hoursA batch payment processing procedure used WHEN OTHERS THEN NULL as its exception handler. A constraint violation on a foreign key silently failed 2,300 payment records over 14 hours before a reconciliation job detected the discrepancy.
SymptomNightly 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.
AssumptionThe team assumed a network issue between the application and database, or a message queue dropping events.
Root causeA 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.
FixReplaced 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 invisibleAlways 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 blocksCode 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 them
Procedure compiles but status shows INVALID in USER_OBJECTSA dependency changed. Run: SELECT name, type, referenced_name, referenced_type FROM USER_DEPENDENCIES WHERE name = 'YOUR_PROC'. Recompile with ALTER PROCEDURE name COMPILE;
ORA-14551: cannot perform a DML operation inside a queryA function called from SELECT contains INSERT/UPDATE/DELETE. Remove DML from the function or wrap it in PRAGMA AUTONOMOUS_TRANSACTION (with caution).
Procedure runs but no rows are updated — zero rows affectedCheck 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.
Application receives ORA-06502: PL/SQL numeric or value errorA 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.
Function returns NULL unexpectedlyCheck for NULL propagation in expressions. In PL/SQL, any expression with a NULL operand returns NULL. Use NVL() or COALESCE() to provide defaults.
Slow performance — procedure takes 30+ secondsEnable 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.

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.

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.sql · SQL
1234567891011121314151617181920212223242526272829303132333435363738394041
-- io.thecodeforge: Function to calculate discounted price
-- Optimized with DETERMINISTIC ifecodeforge.fn_get_discounted_price(
    p_price    IN NUMBER,
    p results are consistent for inputs
CREATE OR REPLACE FUNCTION io.th_discount IN NUMBER
) RETURN NUMBER DETERMINISTIC IS
BEGIN
    -- Functions must return a value and avoid side effects
    RETURN ROUND(p_price * (1 - (p_discount / 100)), 2);
END fn_get_discounted_price;
/

-- io.thecodeforge: Procedure to update project budget
CREATE OR REPLACE PROCEDURE io.thecodeforge.sp_update_budget(
    p_project_id IN NUMBER,
    p_new_budget IN NUMBER
) IS
    v_old_budget NUMBER;
BEGIN
    -- Logical validation before DML
    SELECT budget INTO v_old_budget FROM forge_projects WHERE id = p_project_id;
    
    IF p_new_budget < 0 THEN
        RAISE_APPLICATION_ERROR(-20001, 'Budget cannot be negative.');
    END IF;

    UPDATE forge_projects 
    SET budget = p_new_budget, 
        last_updated = SYSDATE
    WHERE id = p_project_id;
    
    -- Commit logic usually belongs to the caller, but can be here for batch tasks
    COMMIT;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE_APPLICATION_ERROR(-20002, 'Project ID not found.');
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE; -- Propagate error to the application tier
END 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.sql · SQL
12345678910111213141516171819202122
-- io.thecodeforge: Using OUT parameters correctly for metadata retrieval
CREATE OR REPLACE PROCEDURE io.thecodeforge.sp_get_project_meta(
    p_id      IN  NUMBER,
    p_name    OUT VARCHAR2,
    p_version OUT VARCHAR2,
    p_status  OUT VARCHAR2
) IS
BEGIN
    -- 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;
EXCEPTION
    WHEN 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.sql · SQL
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
-- io.thecodeforge: Production-grade exception handling pattern
CREATE OR REPLACE PROCEDURE io.thecodeforge.sp_process_payment(
    p_order_id   IN NUMBER,
    p_amount     IN NUMBER,
    p_status     OUT VARCHAR2
) IS
    v_customer_id NUMBER;
    v_balance     NUMBER;
BEGIN
    -- Validate order exists
    SELECT customer_id INTO v_customer_id
    FROM forge_orders WHERE order_id = p_order_id;

    -- Check balance
    SELECT 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);
    END IF;

    -- Deduct and record
    UPDATE forge_customers SET balance = balance - p_amount
    WHERE customer_id = v_customer_id;

    INSERT INTO forge_payments (order_id, amount, paid_on)
    VALUES (p_order_id, p_amount, SYSDATE);

    COMMIT;
    p_status := 'SUCCESS';

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        ROLLBACK;
        p_status := 'ORDER_NOT_FOUND';
    WHEN DUP_VAL_ON_INDEX THEN
        ROLLBACK;
        p_status := 'DUPLICATE_PAYMENT';
    WHEN OTHERS THEN
        ROLLBACK;
        -- 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 errors
END 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
IfBusiness rule violation (e.g., insufficient balance)
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
🗂 Stored Procedure vs Function in PL/SQL
When to use each subprogram type and their key constraints
FeatureStored ProcedureFunction
Return ValueDoes not return a value via header; uses OUT parameters if needed.Must return exactly one value via the RETURN clause.
Call in SQLCannot be called in a SELECT or WHERE clause.Can be called in SELECT/WHERE if it is 'pure' (no DML).
DML OperationsStandard use case; expected to change database state.Restricted; if called from SQL, it cannot perform DML.
Primary PurposeExecuting business tasks, batch processing, and DML.Computing data, transformations, and returning specific values.
Parameter ModesSupports IN, OUT, and IN OUT modes.Supports IN (standard), OUT/IN OUT (rarely used/discouraged).

🎯 Key Takeaways

  • 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.
  • Procedures are verbs (actions), and Functions are nouns (values). Choose based on whether your primary goal is to change data or calculate it.
  • Encapsulating logic in the database creates a 'Single Source of Truth' for business rules, preventing different apps from calculating the same data differently.
  • Always use explicit exception handling to ensure the integrity of your DML and to provide meaningful feedback to the middleware tier.
  • 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.

⚠ Common Mistakes to Avoid

    Over-abstraction for simple SQL
    Symptom

    A stored procedure wraps a single SELECT statement with no procedural logic. The codebase has hundreds of thin wrappers that add no value — just indirection and maintenance burden.

    Fix

    Only wrap SQL in a procedure when there is added value: security masking, audit logging, input validation, or multi-step logic. A bare SELECT with a procedure name is code bloat, not modularity.

    Dependency blindness on schema changes
    Symptom

    After adding a column or changing a data type, dozens of procedures and functions show INVALID status in USER_OBJECTS. Application starts throwing ORA-04063 or ORA-04068 at runtime.

    Fix

    Query USER_DEPENDENCIES before schema changes to identify all affected subprograms. Recompile invalid objects with: SELECT 'ALTER ' || type || ' ' || name || ' COMPILE;' FROM USER_OBJECTS WHERE status = 'INVALID';

    Swallowing exceptions with WHEN OTHERS THEN NULL
    Symptom

    Procedure completes without error but data is silently missing. Application receives success response. No errors in any log. Reconciliation jobs find discrepancies days later.

    Fix

    Remove WHEN OTHERS THEN NULL entirely. Replace with explicit named exception handlers for expected errors, and a final WHEN OTHERS that logs (SQLERRM, context) and re-raises with RAISE.

    Performing DML inside functions called from SQL
    Symptom

    ORA-14551: cannot perform a DML operation inside a query. The function works fine when called from PL/SQL but fails when used in a SELECT statement.

    Fix

    Remove DML from functions intended for SQL usage. If you need to log inside a function, use PRAGMA AUTONOMOUS_TRANSACTION — but understand it runs in a separate transaction and cannot see uncommitted caller data.

Interview Questions on This Topic

  • QWhat is the fundamental difference between a Procedure and a Function? When is it illegal to use a Function in a SELECT statement?JuniorReveal
    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.
  • QExplain the three parameter modes: IN, OUT, and IN OUT. Which is the default, and how is it passed (by value or reference)?JuniorReveal
    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.
  • QWhat is 'RAISE_APPLICATION_ERROR' and why is it better than just letting Oracle throw a generic system error?Mid-levelReveal
    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.
  • QHow do Stored Procedures improve performance for an application communicating over a network? Mention 'Context Switching'.Mid-levelReveal
    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.
  • QCan a PL/SQL Function be overloaded? If so, what are the requirements regarding parameter signatures?Mid-levelReveal
    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.
  • QWhat is the 'PRAGMA AUTONOMOUS_TRANSACTION' and how would you use it inside a function to log errors without rolling back the main transaction?SeniorReveal
    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.

Frequently Asked Questions

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.

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.

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.

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.

🔥
Naren Founder & Author

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.

← PreviousPL/SQL Cursors ExplainedNext →PL/SQL Triggers Explained
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged