PL/SQL & T-SQL (Oracle and SQL Server) Module 11 – Subprograms, packages and robust error logging
PL/SQL packages, session state and ORA-04068
A package specification declares what callers may use, its body hides the rest, and its state lives once per session until a recompiled body discards it.
What you will learn
- Build packages with a public specification and a private body
- Explain why package state and its initialisation belong to one session
- Handle ORA-04068 after a package body is recompiled, with the RESETTABLE clause
- Use ACCESSIBLE BY to restrict which units may call a helper
Before you start
What you’ll learn · Before you start
What you will learn
- Build packages with a public specification and a private body
- Explain why package state and its initialisation belong to one session
- Handle ORA-04068 after a package body is recompiled, with the RESETTABLE clause
- Use ACCESSIBLE BY to restrict which units may call a helper
Before you start
On this page
Three calls in, a session’s package counter reads 3. A colleague recompiles that package body, and that session’s next call returns no number at all: it fails with ORA-04068, and the counter and everything else the package held for that session are gone.
PL/SQL packages group related code and the data it keeps into one stored unit, in two pieces. A specification declares what callers may use, and a body hides how the work is done. You will build PL/SQL packages in both pieces, follow their state from its first call to the recompilation that discards it, and decide what a specification should expose.
A package has a public specification and a private body
The package specification: The public face of a PL/SQL package: the declarations other code may name, such as subprogram headings, types, constants, exceptions and cursors, each complete enough to be used without reading the code behind it. of account_api declares three items: balance_of, lookups_so_far and forget_cache. Each declaration carries everything a caller needs and nothing about how it works. The package body: The piece of a PL/SQL package that defines everything its specification declares, and may add private items and an initialisation part that nothing outside the package can reach. defines all three, and its headings must repeat the declarations word for word apart from white space, or the body compiles with PLS-00323.
The body is also where the private parts live. account_api keeps a helper, read_balance, and three variables no caller can name: a call of account_api.read_balance does not compile, and neither does a read of account_api.g_lookups. Oracle’s writing guidelines ask for that, declaring only the items invoking programs must see. Step through one caller reaching the package:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The caller reads the spec. account_api declares three items: the function balance_of, the function lookups_so_far and the procedure forget_cache. Each declaration carries everything a caller needs, which is the parameters, the types and the return type.
- The body answers the call. The body defines every item the specification declared, and the two headings must match word for word apart from white space. A mismatch gives PLS-00323 when the body compiles.
- The body keeps its own items. read_balance is declared only in the body, so no caller can name it. A call of account_api.read_balance fails to compile with PLS-00302, which is what hiding the lookup is for.
- Variables make the state. The body also declares g_lookups, g_cached_id and g_cached_balance. Those three values are the package state, and each session that calls the package gets its own copy of them.
- A new body, the same callers. Rewriting the cache inside the body changes nothing a caller named, so units that call account_api need no recompilation. This is why the guidelines put only what callers must see in the specification.
- A new spec, new work. Adding a parameter to balance_of changes the specification, so every unit that calls the public items has to be recompiled. The body stays a black box; the specification is the promise.
The script creates both pieces and calls balance_of twice for account 3; its last two blocks fail on purpose:
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus or SQLcl.
-- One package in two pieces: a specification that declares three public items, and a body that hides a private
-- lookup, two state variables and an initialisation part. Needs the practice schema (run the schema_oracle.sql of
-- practice-schema-and-dialect-basics first). examples/model/session_state_model.py prints the same counters.
-- The script drops the package at the end, so it can run again and leaves the schema as it found it.
SET SERVEROUTPUT ON
-- The specification: everything a caller needs, and nothing about how it works.
CREATE OR REPLACE PACKAGE account_api AUTHID DEFINER IS
FUNCTION balance_of (p_account_id IN NUMBER) RETURN NUMBER;
FUNCTION lookups_so_far RETURN PLS_INTEGER;
PROCEDURE forget_cache;
END account_api;
/
-- The body: the private helper and the two variables are invisible from outside.
CREATE OR REPLACE PACKAGE BODY account_api IS
g_lookups PLS_INTEGER; -- package state, one copy per session
g_cached_id accounts.account_id%TYPE;
g_cached_balance accounts.balance%TYPE;
FUNCTION read_balance (p_account_id IN NUMBER) -- private: declared here, not in the specification
RETURN NUMBER
IS
v_balance accounts.balance%TYPE;
BEGIN
SELECT balance INTO v_balance
FROM accounts
WHERE account_id = p_account_id;
g_lookups := g_lookups + 1;
RETURN v_balance;
END read_balance;
FUNCTION balance_of (p_account_id IN NUMBER)
RETURN NUMBER
IS
BEGIN
IF g_cached_id IS NULL OR g_cached_id <> p_account_id THEN
g_cached_balance := read_balance(p_account_id);
g_cached_id := p_account_id;
DBMS_OUTPUT.PUT_LINE('read from the table');
ELSE
DBMS_OUTPUT.PUT_LINE('answered from the cache');
END IF;
RETURN g_cached_balance;
END balance_of;
FUNCTION lookups_so_far RETURN PLS_INTEGER IS
BEGIN
RETURN g_lookups;
END lookups_so_far;
PROCEDURE forget_cache IS
BEGIN
g_cached_id := NULL;
g_cached_balance := NULL;
END forget_cache;
BEGIN -- the initialisation part: the first reference runs it
g_lookups := 0;
g_cached_id := NULL;
DBMS_OUTPUT.PUT_LINE('account_api: initialised for this session');
END account_api;
/
-- Two calls for the same account, then the counter the specification exposes.
BEGIN
DBMS_OUTPUT.PUT_LINE('balance_of(3) = ' || TO_CHAR(account_api.balance_of(3), 'FM999990.00'));
DBMS_OUTPUT.PUT_LINE('balance_of(3) = ' || TO_CHAR(account_api.balance_of(3), 'FM999990.00'));
DBMS_OUTPUT.PUT_LINE('lookups_so_far = ' || account_api.lookups_so_far);
END;
/
-- By the documented behaviour this prints, in order:
-- account_api: initialised for this session (the initialisation part, once in this session)
-- read from the table
-- balance_of(3) = 760.25
-- answered from the cache
-- balance_of(3) = 760.25
-- lookups_so_far = 1 (one table read, two calls)
-- A caller cannot reach the private parts: both of these fail to compile with PLS-00302.
BEGIN
DBMS_OUTPUT.PUT_LINE(account_api.read_balance(3));
END;
/
BEGIN
DBMS_OUTPUT.PUT_LINE(account_api.g_lookups);
END;
/
-- Expected for each block: PLS-00302: component 'READ_BALANCE' (then 'G_LOOKUPS') must be declared.
DROP PACKAGE account_api; """account_pkg.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""
from oracle_session import Lab
with Lab() as lab:
lab.sqlplus("packages/account_pkg.sql", errors=2) Output
$ sqlplus -s lab@//localhost/FREEPDB1 @packages/account_pkg.sql
Package created.
Package body created.
account_api: initialised for this session
read from the table
balance_of(3) = 760.25
answered from the cache
balance_of(3) = 760.25
lookups_so_far = 1
PL/SQL procedure successfully completed.
DBMS_OUTPUT.PUT_LINE(account_api.read_balance(3));
*
ERROR at line 2:
ORA-06550: line 2, column 36:
PLS-00302: component 'READ_BALANCE' must be declared
ORA-06550: line 2, column 3:
PL/SQL: Statement ignored
Help: https://docs.oracle.com/error-help/db/ora-06550/
DBMS_OUTPUT.PUT_LINE(account_api.g_lookups);
*
ERROR at line 2:
ORA-06550: line 2, column 36:
PLS-00302: component 'G_LOOKUPS' must be declared
ORA-06550: line 2, column 3:
PL/SQL: Statement ignored
Help: https://docs.oracle.com/error-help/db/ora-06550/
Package dropped.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 account_pkg_session.py
Checkpoint
Check: The specification and the body
Package state lives once per session, and the first call starts it
The values of the variables, constants and cursors a package declares, in either piece, are its package state: The values of the variables, constants and cursors a package declares in either of its pieces. One variable is enough to make a package stateful, and each session keeps its own copy of the values., and one variable is enough to make a package stateful. Package state is session state: every session that references a package item gets its own instantiation, carrying its own copy of those values.
Instantiating a package also package initialisation: What Oracle Database does when a session first references a package: it assigns the initial values of the public constants and variables, then runs the initialisation part at the foot of the body. it. Oracle assigns the initial values of the public constants, then of the public variables that declare one, then runs the body’s initialisation part, the block between BEGIN and END at its foot. That happens on a session’s first reference, not on every call, so account_pkg.sql prints its one line once. This session state lives as long as the session does, and two clients calling one stored package keep two counters:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- A calls the package first. Session A calls account_api.balance_of(3). It is the first reference to a package item in this session, so Oracle instantiates account_api for session A.
- Instantiating means initialising. Initialising sets the initial values of the public constants and variables that declare one, then runs the initialisation part of the body: g_lookups becomes 0, g_cached_id NULL, and one line is printed.
- The first read, then the cache. The function reads 760.25 from the accounts table, counts the read and keeps it. A's second call for the same account answers from the cache, so lookups_so_far stays 1.
- B calls the same package. Session B calls balance_of(3) too. Every session that references a package has its own instantiation, so B gets a second copy of the state and its own initialisation runs.
- B reads the table again. B's cache is empty, so its call reads the table even though A read the same row a moment ago. Package state is per session, never shared, so B's counter goes to 1 on its own.
- A asks for another account. A calls balance_of(4), which is not the cached account, so the body reads 41250.50 and replaces the cached pair. A has now made two table reads.
- The counters have parted. lookups_so_far returns 2 in session A and 1 in session B, from the same stored package. Both copies live for as long as their session does.
The model applies the same rules to those six calls. Press Edit, give session B a call for account 4, and run it again: only B’s counter moves.
"""One package, two sessions: Oracle's instantiation rules, modelled.
The model replays account_pkg.sql in two database sessions over the practice
accounts (3 holds 760.25 and 4 holds 41250.50). It applies the documented
rules, so nothing here is a recorded database run.
The rules (PL/SQL Language Reference 26ai, Package Instantiation and
Initialization, and Package State):
1. The first time a session references a package item, Oracle instantiates
the package for that session. Every session gets its own instantiation.
2. Instantiating it initialises it: initial values for public constants,
then for public variables that declare one, then the initialisation part
of the body.
3. The values of the variables, constants and cursors a package declares
are its package state, and a stateful instantiation keeps its own copy
for the life of the session.
Edit CALLS to give the sessions other accounts, or call forget_cache, and
run it again: each session's counter follows only its own calls.
"""
from decimal import Decimal
BALANCE = { # the practice accounts, account_id -> balance
1: Decimal("2500.00"),
2: Decimal("18000.00"),
3: Decimal("760.25"),
4: Decimal("41250.50"),
}
CALLS = [ # (session, call) in the order the two clients run them
("session A", "balance_of(3)"),
("session A", "balance_of(3)"),
("session B", "balance_of(3)"),
("session A", "balance_of(4)"),
("session B", "lookups_so_far"),
("session A", "lookups_so_far"),
]
class Instantiation:
"""One session's copy of the package state, as the body declares it."""
def __init__(self, session):
self.session = session
self.lookups = 0 # g_lookups
self.cached_id = None # g_cached_id
self.cached_balance = None # g_cached_balance
def balance_of(self, account_id):
"""The public function: a table read, or the cached answer."""
if self.cached_id != account_id:
self.cached_balance = BALANCE[account_id]
self.cached_id = account_id
self.lookups += 1
return self.cached_balance, "read from the table"
return self.cached_balance, "answered from the cache"
def reference(sessions, session):
"""Returns the session's instantiation, creating it on first reference."""
if session in sessions:
return sessions[session]
print(f" {session}: no instantiation yet, so Oracle makes one")
print(" initialisation part: g_lookups 0, g_cached_id NULL")
print(" prints: account_api: initialised for this session")
sessions[session] = Instantiation(session)
return sessions[session]
def run(calls):
"""Runs the calls in order and reports what each session sees."""
sessions = {}
for session, call in calls:
print(f"{session} calls account_api.{call}")
state = reference(sessions, session)
if call == "lookups_so_far":
print(f" returns {state.lookups}")
continue
account_id = int(call[len("balance_of("):-1])
balance, how = state.balance_of(account_id)
print(f" prints: {how}")
print(f" returns {balance}")
print()
print("At the end of the two clients' work:")
for session, state in sessions.items():
print(f" {session}: lookups {state.lookups}, cached account "
f"{state.cached_id}")
run(CALLS)
total = sum(1 for session, _ in CALLS if session == "session A")
print(f"session A made {total} of the {len(CALLS)} calls; neither session "
"touched the other's state.") Output
session A calls account_api.balance_of(3)
session A: no instantiation yet, so Oracle makes one
initialisation part: g_lookups 0, g_cached_id NULL
prints: account_api: initialised for this session
prints: read from the table
returns 760.25
session A calls account_api.balance_of(3)
prints: answered from the cache
returns 760.25
session B calls account_api.balance_of(3)
session B: no instantiation yet, so Oracle makes one
initialisation part: g_lookups 0, g_cached_id NULL
prints: account_api: initialised for this session
prints: read from the table
returns 760.25
session A calls account_api.balance_of(4)
prints: read from the table
returns 41250.50
session B calls account_api.lookups_so_far
returns 1
session A calls account_api.lookups_so_far
returns 2
At the end of the two clients' work:
session A: lookups 2, cached account 4
session B: lookups 1, cached account 3
session A made 4 of the 6 calls; neither session touched the other's state.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 session_state_model.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Checkpoint
Check: State and initialisation per session
What takes that state away, and what keeps callers out
Session state outlives every call in its session, with three exceptions, and the first is this lesson’s problem. When the body of an instantiated, stateful package is recompiled, that package’s next call in each holding session discards the session state and raises ORA-04068, existing state of packages has been discarded. The call does no work, and a second, identical call runs normally against a freshly initialised package, so ORA-04068 is the price a release charges the sessions that were holding state. The other two exceptions are the pragma in the table below and the invalidation that dependencies and compilation covers.
Oracle offers four more tools, and the table says what each does:
| Tool | What it does |
|---|---|
| RESETTABLE package: A package marked with the RESETTABLE clause, from Oracle AI Database 26ai Release Update 23.26.0, whose state a session may discard by itself, running the initialisation again at the next reference instead of meeting ORA-04068. | A clause of 26ai Release Update 23.26.0: a session may discard such a unit’s state itself, without ORA-04068. |
| ACCESSIBLE BY: A clause that lists the PL/SQL units allowed to call a package or one of its subprograms; any other caller is refused with PLS-00904, whatever privileges it holds. | A clause listing the units allowed to call a package or one subprogram; any other caller is refused with PLS-00904. |
| SERIALLY_REUSABLE package: A package marked with the SERIALLY_REUSABLE pragma, whose state lives for one server call in a pool the database shares, instead of for the whole session. | A pragma that keeps state for one server call rather than the session, in a shared pool. |
| The writing guidelines | Oracle’s advice on what a specification holds: what callers must see, with procedures in place of variables. |
Which fits depends on whether the value can be rebuilt, who else needs it and who may ask for it.
Checkpoint
Check: What each named tool is for
Reproducing ORA-04068 and the RESETTABLE fix Pro: opens with a Pro, Premium or Ultimate pass
Two clients, one stateful package and one recompilation are enough to produce the error on purpose, and watching it happen tells you exactly which sessions pay for a release. The same timeline then runs twice more, with a replacement that discards nothing and with a package that re-initialises itself.
In this part:
- A two-session timeline of a discarded state
- The errors the next call reports, and the output it lost
- When a replacement discards nothing, and what RESETTABLE changes
1 diagram · 5 runnable examples (Python, SQL) · about 10 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Restricting callers with ACCESSIBLE BY and keeping state minimal Pro: opens with a Pro, Premium or Ultimate pass
A package specification is a contract, and ACCESSIBLE BY is how you keep part of the implementation out of it while still splitting the code across units. The same thinking decides how much state a package should hold at all.
In this part:
- A helper only the API package may call
- Choosing what a specification exposes, with the trade-offs
1 diagram · 2 runnable examples (Python, SQL) · about 12 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: decide what each session holds
Each question gives a package and a moment in a session. Work out what its state holds first:
Practice that adapts to you
Practise this lesson
8 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): what goes in a package specification, and what goes only in its body?
The specification holds the declarations callers need, which are the types, constants, exceptions, cursors and subprogram headings they name. The body holds the code behind them, and may add private items of its own, such as a helper procedure or a variable. It can end with an initialisation part that runs on a session’s first reference, and it is required as soon as the specification declares a cursor or a subprogram. The split pays off when code changes: rewriting the body leaves the calling units compiled, while a change to the specification makes them recompile.
Three longer questions follow: a release that broke a connection pool, a cache that outlived its data, and a helper other teams called:
-
Mid to senior
A release recompiles one package body at the start of a shift. Users of a web application then see errors from actions that worked all week, and the same action succeeds when they try it again. The database team reports no failed statements of its own. What happened, and how would you release that package next time?
-
Mid to staff
A package reads the day's interest rate on its first use and keeps it in a body variable. Operations change the rate during the day, and some sessions go on charging the old one until they disconnect. Where would you keep that rate instead, and how would you prove the fix?
-
Senior to staff
Two other teams found a helper package of yours and now call it directly, skipping the checks your API package does first. Renaming it is out of the question this quarter. How do you stop the direct calls, and what will the other teams see when you do?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does each of a package's two pieces hold?
The specification declares what callers may use, carrying everything a caller needs and nothing about how it works; the body defines those items and hides the private helpers and variables. The body's headings must repeat the declarations word for word apart from white space, or it compiles with PLS-00323.
-
What happens when a caller names an item declared only in the body?
It does not compile: a private helper or variable cannot be reached from outside. Oracle's writing guidelines ask for exactly that, declaring only the items invoking programs must see, with procedures in place of variables.
-
What is a package's state, and how much of it does one session see?
The values of the variables, constants and cursors it declares in either piece, and one variable is enough to make a package stateful. It is session state: every session that references a package item gets its own instantiation with its own copy, so two clients keep two counters.
-
What does instantiating a package do, and how often?
Oracle assigns the initial values of the public constants, then of the public variables that declare one, then runs the body's initialisation part, the block between BEGIN and END at its foot. That happens on the session's first reference, not on every call.
-
What is ORA-04068, and what did the call that reported it do?
Existing state of packages has been discarded: the body of an instantiated, stateful package was recompiled, so that package's next call in each holding session throws away the session state. That call does no work, and a second identical call runs normally against a freshly initialised package.
-
What does the RESETTABLE clause offer?
From 26ai Release Update 23.26.0, a session may discard such a unit's state itself, without ORA-04068.
-
What does ACCESSIBLE BY do?
It lists the units allowed to call a package or one subprogram; any other caller is refused with PLS-00904.
-
What does PRAGMA SERIALLY_REUSABLE change about state?
It keeps the state for one server call rather than for the session, in a shared pool.
Key takeaways
- A specification declares the public items and the body defines them and hides the rest, so only a change to the specification makes the calling units recompile.
- Package state is the values of the variables, constants and cursors a package declares, and every session gets its own copy of them.
- Instantiation initialises a package once per session, before its first call does any work.
- ORA-04068 reports that a session’s package state has been discarded after its body was recompiled, and the call that reports it did no work.
Terms in this lesson
- ACCESSIBLE BY
- A clause that lists the PL/SQL units allowed to call a package or one of its subprograms; any other caller is refused with PLS-00904, whatever privileges it holds.
- package body
- The piece of a PL/SQL package that defines everything its specification declares, and may add private items and an initialisation part that nothing outside the package can reach.
- package initialisation
- What Oracle Database does when a session first references a package: it assigns the initial values of the public constants and variables, then runs the initialisation part at the foot of the body.
- package specification
- The public face of a PL/SQL package: the declarations other code may name, such as subprogram headings, types, constants, exceptions and cursors, each complete enough to be used without reading the code behind it.
- package state
- The values of the variables, constants and cursors a package declares in either of its pieces. One variable is enough to make a package stateful, and each session keeps its own copy of the values.
- RESETTABLE package
- A package marked with the RESETTABLE clause, from Oracle AI Database 26ai Release Update 23.26.0, whose state a session may discard by itself, running the initialisation again at the next reference instead of meeting ORA-04068.
- SERIALLY_REUSABLE package
- A package marked with the SERIALLY_REUSABLE pragma, whose state lives for one server call in a pool the database shares, instead of for the whole session.
References
- PL/SQL Language Reference 26ai: PL/SQL Packages (Oracle)
- PL/SQL Language Reference 26ai: Package State (Oracle)
- PL/SQL Language Reference 26ai: Package Instantiation and Initialization (Oracle)
- PL/SQL Language Reference 26ai: RESETTABLE Clause (Oracle)
- PL/SQL Language Reference 26ai: ACCESSIBLE BY Clause (Oracle)
- PL/SQL Language Reference 26ai: SERIALLY_REUSABLE Packages (Oracle)
- PL/SQL Language Reference 26ai: Package Writing Guidelines (Oracle)
- Database Development Guide 26ai: Schema Object Dependency (Oracle)
- Database Error Messages: ORA-04068 (Oracle)
- Database Error Messages: ORA-04061 (Oracle)
- Database Error Messages: ORA-04065 (Oracle)
- Database Error Messages: ORA-06508 (Oracle)
- Database Error Messages: PLS-00904 (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress