Deep Dive — RESULT_CACHE
A function whose answers are stored in the SGA, shared by every session, and thrown away automatically the moment the underlying table changes.
1. What It Is and When
A RESULT_CACHE function stores each (arguments → result) pair in the server result cache (part of the shared pool). A second call with the same arguments — from any session — returns the stored result without running the body. Oracle tracks which tables the function read and invalidates the cached results when any of them changes.
The sweet spot: small, rarely changing lookup tables (configuration, code tables, tiers, rates) read by many sessions. Not for: big tables, volatile tables, high-cardinality arguments.
The SQL-level sibling is the /*+ RESULT_CACHE */ hint on a query.
2. Syntax
CREATE [OR REPLACE] FUNCTION <name> (<params>) RETURN <type>
RESULT_CACHE [RELIES_ON (<table>, ...)] -- RELIES_ON is ignored since 11.2 (auto-tracked)
IS ... END;
-- management
DBMS_RESULT_CACHE.STATUS -- ENABLED / DISABLED / BYPASS
DBMS_RESULT_CACHE.MEMORY_REPORT -- usage
DBMS_RESULT_CACHE.FLUSH -- drop everything
DBMS_RESULT_CACHE.INVALIDATE(owner, name)
DBMS_RESULT_CACHE.BYPASS(TRUE) -- temporarily disable (e.g. during a bulk load)
-- parameters (DBA): RESULT_CACHE_MAX_SIZE, RESULT_CACHE_MAX_RESULT (% per result), RESULT_CACHE_MODE
-- views: V$RESULT_CACHE_OBJECTS, V$RESULT_CACHE_STATISTICS, V$RESULT_CACHE_DEPENDENCY
Restrictions: no OUT/IN OUT parameters; arguments and result must be scalar, record, or collection of scalars (no LOB, ref cursor, object with LOB); no invoker's rights; no side effects (the body may not run when cached); pipelined functions excluded.
3. Examples on the Course Schema
3a. A lookup that belongs in the cache
CREATE OR REPLACE FUNCTION tier_discount (p_tier VARCHAR2) RETURN NUMBER
RESULT_CACHE
IS
v_pct NUMBER;
BEGIN
SELECT discount_pct INTO v_pct FROM tier_rates WHERE tier = p_tier; -- 4-row table
RETURN v_pct;
END;
/
SELECT order_id, amount * (1 - tier_discount(c.tier) / 100) AS net
FROM orders o JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_date >= DATE '2026-01-01';
4 distinct arguments → 4 cache entries → the SELECT inside the function runs 4 times for 40M rows, across every session. (The function call still happens 40M times; only the body is skipped — see PRAGMA UDF.)
3b. Watching it work
SELECT name, value FROM v$result_cache_statistics
WHERE name IN ('Create Count Success', 'Find Count', 'Invalidation Count');
SELECT id, type, status, name, row_count, scan_count
FROM v$result_cache_objects
WHERE name LIKE '%TIER_DISCOUNT%';
-- status: Published = usable, Invalid = a dependency changed, Bypass = disabled
Find Count climbing while Create Count stays flat = hits.
3c. Invalidation is automatic — and total for that function
UPDATE tier_rates SET discount_pct = 12 WHERE tier = 'GOLD';
COMMIT;
-- every cached result of tier_discount is now Invalid, for every argument, in every session.
-- The next call per argument recomputes and republishes.
Any committed DML on any table the function read invalidates all its results. That is correct, and it is why volatile tables make the cache useless (constant churn).
3d. The wrong candidate
CREATE OR REPLACE FUNCTION customer_tier (p_id NUMBER) RETURN VARCHAR2
RESULT_CACHE
IS ... SELECT tier INTO v FROM customers WHERE customer_id = p_id ... END;
5M distinct arguments → up to 5M cache entries competing for RESULT_CACHE_MAX_SIZE (typically a few hundred MB, shared with the SQL result cache); every update to customers invalidates all of them; and every miss takes the result-cache latch. Under load this is slower than the plain query. For this shape, use a join, or a session-level ASSOCIATIVE ARRAY.
3e. Bypass during a bulk load
BEGIN
DBMS_RESULT_CACHE.BYPASS(TRUE); -- this session: no cache reads or writes
load_tier_rates; -- many updates → would invalidate constantly
DBMS_RESULT_CACHE.BYPASS(FALSE);
END;
4. What Breaks at Scale
- Latch contention. The result cache is protected by a single latch (
Result Cache: RC Latch). Many sessions missing at the same time (high-cardinality arguments, or right after an invalidation) serialize on it. Symptom:latch freewaits on that latch in AWR. - Memory.
RESULT_CACHE_MAX_SIZEis shared by all result-cache users. A function with millions of distinct arguments evicts everyone else's entries. - RAC. Each instance has its own cache; invalidations are propagated, but hits are per instance.
- Invalidation storms. A dependency table updated every few seconds → results are invalid most of the time → all cost, no benefit. Check
Invalidation CountvsFind Count. - Per-call overhead remains. The context switch SQL→PL/SQL still happens per row; the cache only removes the body's work. Combine with
PRAGMA UDF, or inline the lookup as a join when the table is small anyway.
5. Pitfalls
- Caching a function of
SYSDATE,SYS_CONTEXT,USER— the cache ignores those inputs; every session gets the first session's answer. - Side effects in the body (logging, counters) — they stop happening once the result is cached.
- Expecting
RELIES_ONto matter — ignored since 11.2; dependencies are tracked automatically, including through views. RESULT_CACHE_MAX_SIZE = 0— the cache is disabled at instance level; the function silently runs uncached (DBMS_RESULT_CACHE.STATUSsaysDISABLED).- Result larger than
RESULT_CACHE_MAX_RESULT(default 5% of the cache) — not cached, silently. - Functions on tables with VPD policies or on remote tables (database links) — not cached.
- Using it as "free memoization" for a per-row lookup on a big table — see 3d.