Chapter 05 · Memory Architecture

Memory Tuning: SGA & PGA

Memory is the cheapest performance fix available. Adding the right memory in the right place can eliminate thousands of physical I/Os per second. But done wrong — oversizing PGA, undersizing the shared pool, letting AMM make bad decisions — it actively makes things worse.

SGA components Buffer cache advisor Shared pool / ORA-04031 PGA work areas ASMM vs AMM HugePages Memory health dashboard

The Complete Oracle Memory Map

Before tuning anything, you need to know exactly what lives where and why. Every performance problem in memory traces back to one of these structures being too small, too large, or fragmented.

SGA — System Global Area shared by all processes · DB_CACHE_SIZE + SHARED_POOL_SIZE + … Buffer Cache (DB_CACHE_SIZE) Caches data blocks from disk · 60–70% of SGA · tuned via V$DB_CACHE_ADVICE Hit = logical read (µs) · Miss = physical I/O (ms) Shared Pool (SHARED_POOL_SIZE) Library Cache — parsed SQL + execution plans Dictionary Cache — table / column metadata Hard parse = expensive · soft parse = cheap · bind variables = key Large Pool RMAN · parallel · UGA shared server Java Pool Java stored procedures Redo Log Buffer (LOG_BUFFER) Circular buffer · flushed by LGWR on COMMIT · log file sync wait Streams Pool (STREAMS_POOL_SIZE) · GoldenGate / XStream Fixed SGA — latch structures, lock tables, instance state — not tunable PGA (per process) private · not shared Sort Work Area ORDER BY · GROUP BY · spills to temp Hash Join Area Hash joins · spills to temp if full Private SQL Area Bind values · cursor state Background Processes DBWR — writes dirty blocks LGWR — writes redo on COMMIT CKPT · SMON · PMON · ARCH Data files · Redo log files · Control files · Temp tablespace
💡 The critical balance
SGA and PGA compete for the same physical RAM. Too much SGA → tiny PGA work areas → sorts spill to disk. Too little SGA → buffer cache misses → physical I/O storms. Getting this balance right is the heart of memory tuning.

Three Memory Management Modes

Most DBAs don't clearly know which mode their instance is running. This matters because it determines how you tune — and which tools are available.

Manual (MSMM)
DB_CACHE_SIZE = 4G SHARED_POOL_SIZE = 1G SGA_TARGET = 0 MEMORY_TARGET = 0
You control every SGA component exactly
Predictable — no surprise reallocation
Requires ongoing manual adjustment
One pool starved while another wastes
Best for stable, well-understood workloads
Auto SGA (ASMM) ★
SGA_TARGET = 8G MEMORY_TARGET = 0 DB_CACHE_SIZE = 0 (auto) SHARED_POOL = 0 (auto)
Oracle moves memory between SGA pools dynamically
Handles mixed OLTP + batch workloads well
Cannot move memory between SGA and PGA
Compatible with HugePages on Linux
Recommended for most production systems
Auto Memory (AMM)
MEMORY_TARGET = 12G MEMORY_MAX_TARGET = 16G SGA_TARGET = 0 PGA_TARGET = 0
Manages SGA + PGA from one pool
Uses /dev/shm — incompatible with HugePages
Oracle can shrink SGA at worst moment
Higher TLB miss rate on large-memory systems
Avoid on large production systems
SQL — Detect which memory mode you are running
SELECT name, value, description
FROM   v$parameter
WHERE  name IN (
  'memory_target', 'memory_max_target',
  'sga_target',    'sga_max_size',
  'pga_aggregate_target',
  'db_cache_size', 'shared_pool_size',
  'large_pool_size', 'streams_pool_size'
)
ORDER BY name;

-- Quick mode diagnosis:
-- memory_target  > 0              → AMM  (Oracle controls SGA + PGA)
-- sga_target     > 0, memory = 0  → ASMM (Oracle controls SGA pools only)
-- both = 0                        → MSMM (fully manual)

Buffer Cache Advisor

Oracle continuously simulates what would happen to physical reads if you changed the buffer cache size. You don't need to guess — you just need to read the advisor table.

SQL — Read the buffer cache advisor
-- Enable advisor if off
-- ALTER SYSTEM SET db_cache_advice = ON;

SELECT size_for_estimate          AS cache_mb,
       size_factor,
       estd_physical_read_factor  AS read_factor,
       estd_physical_reads        AS estd_reads,
       ROUND((1 - estd_physical_read_factor) * 100, 1) AS pct_io_reduction
FROM   v$db_cache_advice
WHERE  name       = 'DEFAULT'
AND    block_size = (SELECT TO_NUMBER(value)
                     FROM   v$parameter
                     WHERE  name = 'db_block_size')
ORDER BY size_for_estimate;

How to read the advisor output

The table below shows a real-world example. Find the "elbow" — the point where adding more memory gives diminishing returns.

Cache MBSize factorRead factorEst. phys readsI/O reduction
20480.253.8276,400,000−282% (worse)
40960.501.9438,800,000−94%
81921.001.0020,000,000← current
122881.500.7114,200,00029% fewer reads
163842.000.6212,400,00038%
204802.500.6012,000,00040%
245763.000.5911,800,000← diminishing return

The elbow is at 12GB — going from 8GB to 12GB saves 29% of physical reads. Going from 12GB to 16GB saves only another 9%. Invest the 4GB, stop there.

SQL — Apply buffer cache change + verify
-- Apply (dynamic with ASMM — no restart needed)
ALTER SYSTEM SET db_cache_size = 12G SCOPE=BOTH;

-- Verify the change landed
SELECT component,
       current_size/1024/1024/1024      AS current_gb,
       min_size/1024/1024/1024         AS min_gb,
       last_oper_type
FROM   v$sga_dynamic_components
ORDER BY current_size DESC;

-- Which segments are consuming the most buffer cache?
SELECT o.object_name, o.object_type,
       b.cnt * (SELECT TO_NUMBER(value)
                FROM v$parameter
                WHERE name = 'db_block_size') / 1024 / 1024 AS cached_mb
FROM  (SELECT obj, COUNT(*) cnt FROM v$bh WHERE dirty='N' GROUP BY obj) b
JOIN   dba_objects o ON o.data_object_id = b.obj
WHERE  b.cnt > 100
ORDER BY b.cnt DESC
FETCH FIRST 20 ROWS ONLY;

Hit Ratio vs Physical Reads Per Second

The buffer cache hit ratio is a starting point — but it can mislead. A 99% hit ratio on a system doing 500,000 logical reads/sec still means 5,000 physical reads/sec. Always look at both.

SQL — Hit ratio AND physical reads per second
-- Buffer cache hit ratio (target: > 95% on OLTP)
SELECT ROUND(
  (1 - (p.value / (c.value + g.value))) * 100, 2
) AS hit_ratio_pct
FROM   v$sysstat p, v$sysstat c, v$sysstat g
WHERE  p.name = 'physical reads'
AND    c.name = 'db block gets'
AND    g.name = 'consistent gets';

-- Physical reads per second since startup (the absolute measure)
SELECT ROUND(p.value /
       GREATEST(((SYSDATE - s.startup_time) * 86400), 1)) AS phys_reads_per_sec
FROM   v$sysstat p, v$instance s
WHERE  p.name = 'physical reads';

Shared Pool Tuning

The shared pool starves in two ways: too small (ORA-04031), or too fragmented (lots of free chunks but no large contiguous one). Both cause hard parse failures and application errors.

SQL — Shared pool utilization and advisor
-- What is eating the shared pool?
SELECT name, bytes/1024/1024 AS mb,
       ROUND(bytes * 100 / SUM(bytes) OVER (), 1) AS pct
FROM   v$sgastat
WHERE  pool = 'shared pool'
ORDER BY bytes DESC
FETCH FIRST 15 ROWS ONLY;

-- Critical: free memory in shared pool
SELECT bytes/1024/1024 AS free_mb
FROM   v$sgastat
WHERE  pool = 'shared pool'
AND    name = 'free memory';
-- Below 50MB on a busy system = risk of ORA-04031

-- Shared pool advisor
SELECT shared_pool_size_for_estimate  AS pool_mb,
       estd_lc_size                   AS lib_cache_mb,
       estd_lc_memory_object_hits     AS estimated_hits,
       estd_lc_time_saved_factor      AS time_saved_factor
FROM   v$shared_pool_advice
ORDER BY shared_pool_size_for_estimate;

-- avg_execs_per_sql < 2 = bind variable problem / library cache bloat
SELECT COUNT(DISTINCT sql_id)           AS unique_sqls,
       SUM(executions)                    AS total_execs,
       ROUND(SUM(executions) /
         COUNT(DISTINCT sql_id), 1)      AS avg_execs_per_sql
FROM   v$sqlarea
WHERE  parsing_schema_name NOT IN ('SYS','SYSTEM','DBSNMP');

ORA-04031 — Shared Pool Out of Memory

This error means Oracle cannot allocate a contiguous chunk in the shared pool. It crashes sessions mid-execution. Before just increasing SHARED_POOL_SIZE, diagnose the root cause.

SQL — ORA-04031 diagnosis and fix
-- Root cause 1: bind variable problem — library cache bloat
-- Look for force_matching_signature with thousands of variants
SELECT force_matching_signature,
       COUNT(DISTINCT sql_id)  AS variants,
       SUM(sharable_mem)/1024/1024 AS total_mb,
       MIN(SUBSTR(sql_text,1,80)) AS sample_sql
FROM   v$sqlarea
WHERE  parsing_schema_name NOT IN ('SYS','SYSTEM')
GROUP BY force_matching_signature
HAVING COUNT(DISTINCT sql_id) > 100
ORDER BY total_mb DESC
FETCH FIRST 10 ROWS ONLY;

-- Root cause 2: large non-shareable objects eating shared pool
SELECT name, namespace,
       sharable_mem/1024  AS kb,
       loads, executions, invalidations
FROM   v$db_object_cache
WHERE  sharable_mem > 102400   -- > 100KB objects
AND    type NOT IN ('CURSOR','PACKAGE','PROCEDURE','FUNCTION')
ORDER BY sharable_mem DESC;

-- Fix: pin critical packages to prevent aging out
EXEC DBMS_SHARED_POOL.KEEP('APPS.ORDER_PROCESSING_PKG', 'P');
EXEC DBMS_SHARED_POOL.KEEP('APPS.PRICING_ENGINE_PKG',   'P');

-- Verify what is pinned
SELECT owner, name, namespace, kept
FROM   v$db_object_cache
WHERE  kept = 'YES';
🔎 Real scenario — ORA-04031 at peak hours
Application errors spike every morning at 9 AM with ORA-04031. You check force_matching_signature and find 47,000 unique SQL variants — all identical except for WHERE clause literals. A Java developer used string concatenation instead of PreparedStatement. The library cache fills with 47,000 non-reusable cursors. Fix: enforce bind variables in the app. Immediately: set CURSOR_SHARING = FORCE as a temporary bandage — Oracle auto-creates bind variables. Long-term: fix the application code.

PGA Tuning

PGA tuning has the most dramatic performance impact per byte added. A sort that spills to disk can be 100× slower than one that stays in memory. The key metric is the work area execution mode breakdown.

Work area execution modes

Optimal mode
Entire sort/hash fits in RAM
Perfect — target 95%+
One-pass mode
One temp spill — tolerable
Acceptable occasionally
Multi-pass mode
Multiple disk passes
ALARM — even 1% is serious
SQL — PGA work area mode breakdown + advisor
-- Work area mode breakdown — the most important PGA metric
SELECT name, value,
       CASE
         WHEN name = 'work areas executed in optimal mode'
           THEN 'No spill — all in memory'
         WHEN name = 'work areas executed in onepass mode'
           THEN 'One temp spill — tolerable'
         WHEN name = 'work areas executed in multipass mode'
           THEN 'ALARM — heavy disk spill'
       END AS meaning
FROM   v$pgastat
WHERE  name LIKE 'work areas%';

-- Current PGA allocation snapshot
SELECT name, value/1024/1024 AS mb
FROM   v$pgastat
WHERE  name IN (
  'aggregate PGA target parameter',
  'total PGA inuse',
  'total PGA allocated',
  'maximum PGA allocated'
);

-- PGA target advisor — find the elbow
SELECT pga_target_for_estimate/1024/1024  AS pga_target_mb,
       pga_target_factor,
       estd_pga_cache_hit_percentage       AS estd_hit_pct,
       estd_overalloc_count
FROM   v$pga_target_advice
ORDER BY pga_target_for_estimate;

Reading the PGA advisor output

PGA target MBPGA factorCache hit %Overalloc count
5120.12523%847 ← severe overallocation
10240.2541%312
20480.5067%89
40961.0081%0 ← current
81922.0091%0 ← elbow
163844.0094%0
327688.0095%0 (minimal gain)

The elbow is at 8GB — going from 4GB to 8GB buys 10% improvement in cache hit rate. Going to 16GB adds only 3% more. Invest the 8GB.

SQL — Apply PGA target change
-- Apply (dynamic — no restart needed)
ALTER SYSTEM SET pga_aggregate_target = 8G SCOPE=BOTH;

-- 12c+: set a hard ceiling to prevent runaway PGA
ALTER SYSTEM SET pga_aggregate_limit = 16G SCOPE=BOTH;

Finding and Fixing PGA Spills

SQL — Sessions spilling to temp right now + historical
-- Sessions currently spilling to temp
SELECT s.sid, s.username, s.sql_id,
       t.tablespace,
       t.blocks * 8 / 1024  AS temp_mb_used
FROM   v$session       s
JOIN   v$tempseg_usage t ON t.session_addr = s.saddr
ORDER BY temp_mb_used DESC;

-- Historical: SQL with the most disk sorts
SELECT sql_id, sorts, disk_sorts,
       ROUND(disk_sorts * 100 / NULLIF(sorts, 0), 1) AS pct_disk_sorts,
       ROUND(elapsed_time/1e6/NULLIF(executions,0), 2) AS avg_ela_sec
FROM   v$sqlarea
WHERE  disk_sorts > 0
AND    executions > 10
ORDER BY disk_sorts DESC
FETCH FIRST 15 ROWS ONLY;

-- Temp usage by user right now
SELECT u.username,
       SUM(s.blocks) * 8 / 1024  AS temp_mb_used
FROM   v$sort_usage s
JOIN   dba_users    u ON u.user_id = s.user#
GROUP BY u.username
ORDER BY temp_mb_used DESC;
🔎 Real scenario — nightly batch consuming 200GB temp
Nightly batch is using 200GB of temp. You check v$sqlarea for disk_sorts and find one SQL with disk_sorts = 14,000 per night — a report sorting 50M rows. Fix in layers: (1) Immediate: raise pga_aggregate_target so the sort gets more RAM. (2) Better: add an index that delivers rows pre-sorted, eliminating the sort entirely. (3) Best: rewrite using analytic functions with PARTITION clause — processes smaller sorted subsets instead of sorting the entire 50M row result.

Memory Sizing Formula

This is the framework experienced DBAs use when sizing a new Oracle instance or reviewing an existing allocation.

Example: 64GB server, OLTP workload
OSLeave 4GB for OS + filesystem cache. Available for Oracle = 60GB.
SGA 67%SGA_TARGET = 40GB. OLTP workload is buffer-cache-heavy.
Cache 70%Buffer cache = 28GB. Biggest component — where most OLTP blocks live.
Pool 20%Shared pool = 8GB. Enough for library cache + dictionary. Pin critical packages.
Large 5%Large pool = 2GB. RMAN backup streams + parallel query UGA.
PGA 27%PGA_AGGREGATE_TARGET = 16GB. Generous for sorts and hash joins.
LimitPGA_AGGREGATE_LIMIT = 20GB. Hard ceiling prevents runaway sessions.
💡 SGA/PGA split by workload type
OLTP-heavy → SGA 70%, PGA 30% (buffer cache dominates).
Mixed OLTP + batch → SGA 60%, PGA 40%.
Analytics / DW → SGA 40%, PGA 60% (large sorts dominate).
SQL — Verify current allocation vs model
SELECT component,
       current_size/1024/1024/1024       AS current_gb,
       user_specified_size/1024/1024/1024 AS specified_gb,
       last_oper_type, last_oper_time
FROM   v$sga_dynamic_components
ORDER BY current_size DESC;

HugePages — The Hidden Performance Layer

This is something most junior DBAs don't know about. On Linux, default memory management uses 4KB pages. For a 40GB SGA, that's 10 million page table entries. The TLB (CPU's cache for page table entries) gets overwhelmed. HugePages uses 2MB pages — just 20,000 entries for the same SGA. The result: 5–15% performance improvement on large-memory OLTP systems.

🚨 HugePages and AMM are incompatible on Linux
AMM uses /dev/shm (tmpfs), which does NOT support HugePages. This is the biggest practical reason to use ASMM (SGA_TARGET) instead of AMM (MEMORY_TARGET) on Linux production systems. If you're on AMM, you're giving up HugePages — and the performance that comes with it.
SQL + Shell — Calculate and configure HugePages
-- Step 1: Calculate required HugePages from SQL*Plus
SELECT SUM(value)/1024/1024         AS sga_mb,
       CEIL(SUM(value) / 2097152)  AS hugepages_needed
FROM   v$sga;

-- Step 2: Check current HugePages on OS (run as root)
-- grep -i hugepages /proc/meminfo
-- HugePages_Total should be non-zero and >= hugepages_needed above

-- Step 3: Set HugePages in /etc/sysctl.conf (OS level)
-- vm.nr_hugepages = <hugepages_needed>
-- sysctl -p   (apply without reboot)

-- Step 4: Disable AMM if currently using it
ALTER SYSTEM SET memory_target      = 0  SCOPE=SPFILE;
ALTER SYSTEM SET memory_max_target  = 0  SCOPE=SPFILE;
ALTER SYSTEM SET sga_target         = 40G SCOPE=SPFILE;
ALTER SYSTEM SET sga_max_size       = 48G SCOPE=SPFILE;
-- Restart required for SGA_MAX_SIZE and to activate HugePages

Memory Health Dashboard

Run this single query on any Oracle instance to get an immediate health report of all four memory dimensions.

SQL — Complete memory health check
SELECT 'Buffer Cache Hit Ratio' AS metric,
       ROUND((1-(p.value/(c.value+g.value)))*100,2)||'%' AS value,
       CASE WHEN (1-(p.value/(c.value+g.value)))*100 < 90
            THEN 'WARNING — increase DB_CACHE_SIZE'
            ELSE 'OK' END AS status
FROM   v$sysstat p, v$sysstat c, v$sysstat g
WHERE  p.name='physical reads'
AND    c.name='db block gets'
AND    g.name='consistent gets'
UNION ALL
SELECT 'Shared Pool Free %',
       ROUND(f.bytes*100/t.bytes,1)||'%',
       CASE WHEN f.bytes*100/t.bytes < 10
            THEN 'WARNING — ORA-04031 risk'
            ELSE 'OK' END
FROM  (SELECT SUM(bytes) bytes FROM v$sgastat
       WHERE pool='shared pool' AND name='free memory') f,
      (SELECT SUM(bytes) bytes FROM v$sgastat
       WHERE pool='shared pool') t
UNION ALL
SELECT 'PGA Optimal Work Areas %',
       ROUND(opt.value*100/NULLIF(tot.value,0),1)||'%',
       CASE WHEN opt.value*100/NULLIF(tot.value,0) < 90
            THEN 'WARNING — increase PGA_AGGREGATE_TARGET'
            ELSE 'OK' END
FROM   v$pgastat opt, v$pgastat tot
WHERE  opt.name = 'work areas executed in optimal mode'
AND    tot.name = 'work areas executions - total'
UNION ALL
SELECT 'Hard Parse Ratio %',
       ROUND(h.value*100/NULLIF(t.value,0),1)||'%',
       CASE WHEN h.value*100/NULLIF(t.value,0) > 10
            THEN 'WARNING — check bind variables'
            ELSE 'OK' END
FROM   v$sysstat h, v$sysstat t
WHERE  h.name='parse count (hard)'
AND    t.name='parse count (total)';

Self-Check Questions

  • Q1 Your system is configured with MEMORY_TARGET = 24G on Linux. A colleague says performance is inexplicably worse than the old server despite identical SQL and more RAM. What Linux-level feature is likely disabled as a result of using AMM, and how do you fix it?
  • Q2 The PGA advisor shows estd_overalloc_count = 312 at your current PGA_AGGREGATE_TARGET. What does an overalloc_count greater than 0 mean, and why is it dangerous for system stability?
  • Q3 You check v$pgastat and see work areas executed in multipass mode = 847 over the last hour. What exact problem is this telling you, and what are the two ways to fix it?
  • Q4 Your shared pool free memory is consistently below 20MB on a 4GB shared pool. Before just increasing SHARED_POOL_SIZE, what two root causes should you investigate first — and what queries do you run for each?
  • Q5 You increase DB_CACHE_SIZE from 8GB to 16GB. The buffer cache hit ratio improves from 91% to 93% but physical reads per second barely changed. What does this tell you about the workload, and where should you look instead?