This article is a practical tuning script library. Each script targets a specific, common real-world performance scenario — not abstract examples, but the situations that actually appear at 2am during an incident. Scripts are annotated with where they run, what the output means, and what action to take.


Reference Environment

Single Instance:
  Host:      ora19c01.prod.example.com
  DB Name:   ORCL19C
  Oracle:    19.22.0.0  (Oracle Linux 8)
  Memory:    256 GB RAM, 32 vCPU
  Storage:   Local NVMe (non-Exadata)

CDB Environment:
  Host:      cdb01.prod.example.com  (Exadata VM)
  CDB Name:  PRODCDB
  PDBs:      PDB_OLTP, PDB_DWH, PDB_REPORTING
  Oracle:    19.22.0.0
  Memory:    512 GB RAM (Exadata X8M-2 node)

Scenario 1: “The Database Was Slow from 09:00 to 09:30 This Morning”

Situation: Application team reports intermittent slowness on cdb01. You need to identify what happened between 09:00 and 09:30.

-- [[email protected] ~] as sysdba
-- Step 1: Find the AWR snapshot IDs covering 09:00–09:30
SELECT snap_id,
       TO_CHAR(begin_interval_time, 'YYYY-MM-DD HH24:MI') AS begin_time,
       TO_CHAR(end_interval_time,   'YYYY-MM-DD HH24:MI') AS end_time
FROM dba_hist_snapshot
WHERE begin_interval_time >= TO_TIMESTAMP('2026-03-01 08:45', 'YYYY-MM-DD HH24:MI')
  AND end_interval_time   <= TO_TIMESTAMP('2026-03-01 09:45', 'YYYY-MM-DD HH24:MI')
ORDER BY snap_id;
SNAP_ID  BEGIN_TIME        END_TIME
-------  ----------------  ----------------
    882  2026-03-01 08:30  2026-03-01 09:00
    883  2026-03-01 09:00  2026-03-01 09:30   ← This is the problem window
    884  2026-03-01 09:30  2026-03-01 10:00
-- Step 2: ASH activity timeline in 5-minute buckets for the problem window
-- Shows AAS (Average Active Sessions) per bucket — reveals where the spike was
SELECT TO_CHAR(TRUNC(sample_time, 'MI') -
               MOD(EXTRACT(MINUTE FROM sample_time), 5) * INTERVAL '1' MINUTE,
               'HH24:MI') AS bucket,
       COUNT(*) AS ash_samples,
       RPAD('█', ROUND(COUNT(*) / 5), '█') AS chart,   -- ASCII bar chart
       COUNT(CASE WHEN session_state = 'WAITING' THEN 1 END) AS waiting,
       COUNT(CASE WHEN session_state = 'ON CPU'  THEN 1 END) AS on_cpu
FROM dba_hist_active_sess_history
WHERE sample_time BETWEEN TO_TIMESTAMP('2026-03-01 09:00', 'YYYY-MM-DD HH24:MI')
                      AND TO_TIMESTAMP('2026-03-01 09:30', 'YYYY-MM-DD HH24:MI')
  AND con_id > 0
GROUP BY TRUNC(sample_time, 'MI') -
         MOD(EXTRACT(MINUTE FROM sample_time), 5) * INTERVAL '1' MINUTE
ORDER BY 1;
BUCKET  ASH_SAMPLES  CHART                          WAITING  ON_CPU
------  -----------  -----------------------------  -------  ------
09:00            12  ████                                 8       4
09:05            15  ████                                11       4
09:10            89  █████████████████████████████      85       4   ← SPIKE
09:15           102  ██████████████████████████████████ 98       4   ← PEAK
09:20            78  ██████████████████████████         74       4
09:25            22  ███████                            18       4

The spike is at 09:10–09:20. Most sessions are WAITING.

-- Step 3: What were they waiting for?
SELECT event,
       COUNT(*) AS samples,
       RPAD('█', ROUND(COUNT(*) * 40 / MAX(COUNT(*)) OVER (), 0), '█') AS pct_chart
FROM dba_hist_active_sess_history
WHERE sample_time BETWEEN TO_TIMESTAMP('2026-03-01 09:10', 'YYYY-MM-DD HH24:MI')
                      AND TO_TIMESTAMP('2026-03-01 09:20', 'YYYY-MM-DD HH24:MI')
  AND session_state = 'WAITING'
  AND wait_class   != 'Idle'
GROUP BY event
ORDER BY samples DESC
FETCH FIRST 10 ROWS ONLY;
EVENT                                       SAMPLES  PCT_CHART
------------------------------------------  -------  ----------------------------------------
enq: TX - row lock contention                    91  ████████████████████████████████████████
log file sync                                     7  ███
db file sequential read                           3  █

Finding: Row lock contention (enq: TX) dominated the spike. Someone held a lock from 09:08 to 09:22.

-- Step 4: Find the blocking session (from ASH historical data)
SELECT h1.sample_time,
       h1.session_id                     AS blocked_sid,
       h1.blocking_session               AS blocker_sid,
       h1.sql_id                         AS blocked_sql,
       h2.sql_id                         AS blocker_sql,
       h1.current_obj#,
       o.object_name                     AS locked_object
FROM dba_hist_active_sess_history h1
LEFT JOIN dba_hist_active_sess_history h2
  ON  h1.blocking_session  = h2.session_id
  AND h1.sample_id         = h2.sample_id
  AND h1.inst_id           = h2.inst_id
LEFT JOIN dba_objects o ON h1.current_obj# = o.object_id
WHERE h1.sample_time BETWEEN TO_TIMESTAMP('2026-03-01 09:08', 'YYYY-MM-DD HH24:MI')
                         AND TO_TIMESTAMP('2026-03-01 09:22', 'YYYY-MM-DD HH24:MI')
  AND h1.event LIKE 'enq: TX%'
ORDER BY h1.sample_time
FETCH FIRST 20 ROWS ONLY;

Action: The blocker SQL ID reveals a long-running transaction (possibly an uncommitted batch update on ORDERS). Fix: ensure batch jobs COMMIT frequently; add row-level lock timeout at the application layer.


Scenario 2: CPU Saturation on PDB_DWH — All Cores at 100%

Situation: ora19c01 single instance shows 100% CPU. Application response time has tripled.

-- [[email protected] ~] as sysdba
-- Real-time: what is consuming CPU right now?
SELECT s.sql_id,
       SUBSTR(t.sql_text, 1, 60)           AS sql_text,
       s.status,
       s.username,
       s.module,
       s.event,
       s.seconds_in_wait                   AS wait_secs
FROM v$session s
JOIN v$sql t ON s.sql_id = t.sql_id AND s.sql_child_number = t.child_number
WHERE s.status    = 'ACTIVE'
  AND s.username IS NOT NULL
  AND s.type      = 'USER'
ORDER BY s.seconds_in_wait DESC
FETCH FIRST 20 ROWS ONLY;
-- Which SQL is consuming the most CPU in the last 15 minutes?
SELECT sql_id,
       ROUND(SUM(cpu_time_delta) / 1e6, 1)     AS cpu_secs,
       SUM(executions_delta)                   AS execs,
       ROUND(SUM(cpu_time_delta) /
             NULLIF(SUM(executions_delta),0) / 1000, 0) AS avg_cpu_ms,
       ROUND(SUM(buffer_gets_delta) /
             NULLIF(SUM(executions_delta),0))  AS avg_lio
FROM dba_hist_sqlstat
WHERE snap_id >= (SELECT MAX(snap_id) - 1 FROM dba_hist_snapshot)
GROUP BY sql_id
ORDER BY cpu_secs DESC
FETCH FIRST 10 ROWS ONLY;
SQL_ID         CPU_SECS  EXECS  AVG_CPU_MS  AVG_LIO
-------------- --------  -----  ----------  -------
gx8n2bfm3q9rk   1842.3      1     1842300   98000000   ← Single query, 1.8K CPU secs
4np7zmd1xq0tw     421.5    124        3398     450000
...

The top SQL ran once and consumed 1842 seconds of CPU — a massive analytical query. Check the plan:

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR('gx8n2bfm3q9rk', NULL, NULL, 'ALL'));

Likely finding: Full table scan with no partition pruning, or a hash join with massive cardinality explosion causing temp space spill. Refer to the AWR/ASH methodology article for plan analysis.


Scenario 3: CDB Memory Pressure — One PDB Starving Others

Situation: PDB_OLTP on cdb01 has normal response times until noon, when PDB_DWH starts a batch run and OLTP latency spikes.

-- [[email protected] ~] as sysdba (CDB level)
-- Check buffer cache usage per PDB (which PDB owns the most buffer cache?)
SELECT s.con_id,
       p.pdb_name,
       COUNT(*) AS buffers_cached,
       ROUND(COUNT(*) * 8192 / 1024 / 1024, 0) AS cache_mb
FROM v$bh b
JOIN v$session s ON b.inst_id = USERENV('INSTANCE')  -- simplified
JOIN cdb_pdbs p ON s.con_id = p.con_id
GROUP BY s.con_id, p.pdb_name
ORDER BY buffers_cached DESC;
-- Better: use V$PDB_CACHE (available in 19c+)
SELECT con_id,
       name,
       ROUND(value / 1024 / 1024, 0) AS value_mb
FROM v$con_sysmetric
WHERE metric_name = 'Buffer Cache Hit Ratio'
ORDER BY con_id;
-- Check PGA usage per PDB
SELECT p.pdb_name,
       ROUND(SUM(pga_alloc_mem) / 1024 / 1024, 0) AS pga_alloc_mb,
       ROUND(SUM(pga_used_mem)  / 1024 / 1024, 0) AS pga_used_mb,
       COUNT(*)                                     AS sessions
FROM v$process pr
JOIN v$session s ON pr.addr = s.paddr
JOIN cdb_pdbs p  ON s.con_id = p.con_id
WHERE s.type = 'USER'
GROUP BY p.pdb_name
ORDER BY pga_alloc_mb DESC;
PDB_NAME      PGA_ALLOC_MB  PGA_USED_MB  SESSIONS
------------- ------------  -----------  --------
PDB_DWH              42870        41200         4   ← DWH consuming 42 GB PGA
PDB_OLTP              3210         3100       120
PDB_REPORTING          840          820        15

Finding: PDB_DWH is consuming 42 GB of PGA (large parallel hash joins). This is starving PDB_OLTP.

-- Fix: Set per-PDB PGA limit to prevent noisy-neighbour effect
ALTER PLUGGABLE DATABASE PDB_DWH SET PGA_AGGREGATE_LIMIT = 20G;
ALTER PLUGGABLE DATABASE PDB_OLTP SET PGA_AGGREGATE_LIMIT = 10G;

-- Set per-PDB SGA limits (prevents buffer cache monopolisation)
ALTER PLUGGABLE DATABASE PDB_DWH  SET SGA_MIN_SIZE = 4G, SGA_TARGET = 16G;
ALTER PLUGGABLE DATABASE PDB_OLTP SET SGA_MIN_SIZE = 8G, SGA_TARGET = 32G;

Scenario 4: Redo Log Write Bottleneck (log file sync Spikes)

Situation: log file sync appears in the top 5 wait events on ora19c01.

-- [[email protected] ~] as sysdba
-- Check log file sync average wait time
SELECT event,
       total_waits,
       ROUND(time_waited / total_waits / 100, 2) AS avg_wait_ms,
       ROUND(time_waited / 100, 0)               AS total_wait_secs
FROM v$system_event
WHERE event = 'log file sync'
ORDER BY time_waited DESC;
EVENT            TOTAL_WAITS  AVG_WAIT_MS  TOTAL_WAIT_SECS
---------------  -----------  -----------  ---------------
log file sync        2847319         18.4            52390

18.4 ms average for log file sync is high. Target is < 5ms on SSD.

-- Check redo log write time (log file parallel write)
SELECT event,
       total_waits,
       ROUND(time_waited / total_waits / 100, 2) AS avg_wait_ms
FROM v$system_event
WHERE event = 'log file parallel write';
EVENT                     TOTAL_WAITS  AVG_WAIT_MS
-----------------------  -----------  -----------
log file parallel write       847319          2.1

Finding: log file parallel write is 2.1ms (fast write to disk) but log file sync is 18.4ms. The gap indicates a commit batching problem — sessions commit after every single row, causing massive log file sync accumulation.

-- Identify high-commit-rate SQL
SELECT sql_id,
       ROUND(executions / (SYSDATE - to_date('2026-03-01', 'YYYY-MM-DD')) / 86400, 0) AS exec_per_sec,
       command_type
FROM v$sqlstats
WHERE command_type = 6   -- COMMIT
  AND executions > 10000
ORDER BY executions DESC
FETCH FIRST 10 ROWS ONLY;

Action: Batch commits in the application (commit every 1000 rows instead of every row). For the database side, consider group commit tuning:

-- Enable deferred commit group flush (reduces sync overhead)
ALTER SYSTEM SET "_use_adaptive_log_file_sync" = FALSE;
ALTER SYSTEM SET commit_logging = 'BATCH';
ALTER SYSTEM SET commit_wait    = 'NOWAIT';
-- Note: NOWAIT means asynchronous commit — only use if data loss on crash is acceptable for this workload

Scenario 5: SGA Sizing Validation for a CDB

Situation: You need to validate whether the SGA is sized correctly for PRODCDB or if the buffer cache is too small (causing excessive physical reads).

-- [[email protected] ~] as sysdba
-- Buffer cache hit ratio (current)
SELECT ROUND(
         (1 - (SUM(CASE WHEN metric_name = 'Physical Reads Per Sec'       THEN average ELSE 0 END)
             / NULLIF(SUM(CASE WHEN metric_name = 'Logical Reads Per Sec' THEN average ELSE 0 END), 0))
         ) * 100, 2) AS buffer_cache_hit_pct
FROM v$sysmetric_history
WHERE metric_name IN ('Physical Reads Per Sec', 'Logical Reads Per Sec')
  AND begin_time >= SYSDATE - 1/24;
BUFFER_CACHE_HIT_PCT
--------------------
               94.21

94.2% is borderline — 96%+ is the target for OLTP workloads.

-- Buffer cache sizing advisor: what would hit ratio be at different cache sizes?
SELECT size_for_estimate        AS cache_size_mb,
       estd_physical_read_factor,
       ROUND((1 - estd_physical_read_factor) * 100, 1) AS estimated_hit_pct
FROM v$db_cache_advice
WHERE name = 'DEFAULT'
  AND block_size = (SELECT value FROM v$parameter WHERE name = 'db_block_size')
  AND advice_status = 'ON'
ORDER BY size_for_estimate;
CACHE_SIZE_MB  ESTD_PHYSICAL_READ_FACTOR  ESTIMATED_HIT_PCT
-------------  -------------------------  -----------------
        16384                       2.10               52.4
        32768                       1.45               65.5
        49152                       1.10               80.2
        65536                       1.00               94.2   ← Current
        81920                       0.92               96.8
        98304                       0.88               97.9   ← Sweet spot
       114688                       0.87               98.1   (diminishing returns)

Chart (ASCII):

Buffer Cache Hit % vs Size
100% |                                         ●──●──●
 98% |                                    ●
 96% |                               ●
 94% |                          ● (current)
 80% |                 ●
 65% |       ●
 52% | ●
     +------+------+------+------+------+------+------+
       16GB   32GB   48GB   64GB   80GB   96GB  112GB
                                         ↑
                               Sweet spot: +32GB SGA
                               gives +3.7% hit rate improvement

Action: Increase SGA_TARGET by 32 GB to reach the sweet spot:

ALTER SYSTEM SET SGA_TARGET  = 98304M SCOPE=BOTH;
ALTER SYSTEM SET SGA_MAX_SIZE = 98304M SCOPE=SPFILE;
-- Restart required for SGA_MAX_SIZE increase

Scenario 6: Shared Pool Pressure — Hard Parse Spikes

Situation: library cache: mutex X appears in the wait profile. CPU is high and hard parses have spiked.

-- [[email protected] ~] as sysdba
-- Check hard parse rate
SELECT metric_name,
       ROUND(value, 2) AS current_value
FROM v$sysmetric
WHERE metric_name IN (
    'Hard Parses Per Sec',
    'Soft Parses Per Sec',
    'Library Cache Hit Ratio',
    'Shared Pool Free %'
);
METRIC_NAME                 CURRENT_VALUE
--------------------------  -------------
Hard Parses Per Sec               1247.3   ← Very high (target < 10)
Soft Parses Per Sec              12430.5
Library Cache Hit Ratio              91.2  ← Below target (99%+)
Shared Pool Free %                    2.1  ← Critically low
-- Find SQL statements NOT using bind variables (causing excessive hard parsing)
SELECT sql_id,
       SUBSTR(sql_text, 1, 80) AS sql_text_sample,
       parse_calls,
       executions,
       ROUND(parse_calls / NULLIF(executions, 0), 2) AS parse_ratio
FROM v$sql
WHERE parse_calls > 100
  AND (SELECT COUNT(*) FROM v$sql s2
       WHERE SUBSTR(s2.sql_text, 1, 40) = SUBSTR(v$sql.sql_text, 1, 40)) > 5
  AND executions > 0
ORDER BY parse_calls DESC
FETCH FIRST 15 ROWS ONLY;

Action: Enable cursor sharing (temporary fix) and address literal SQL in the application:

-- Temporary fix: force cursor sharing for literal SQL
ALTER SYSTEM SET CURSOR_SHARING = FORCE SCOPE=BOTH;

-- Permanent fix: increase shared pool size for high-parse-rate systems
ALTER SYSTEM SET SHARED_POOL_SIZE = 8G SCOPE=BOTH;

-- Enable cursor cache at session level for PL/SQL-heavy workloads
ALTER SYSTEM SET SESSION_CACHED_CURSORS = 50 SCOPE=BOTH;

Scenario 7: Full Diagnostic Script — “I Need a Quick Health Snapshot”

-- [[email protected] ~] as sysdba
-- File: /u01/scripts/quick_health.sql
-- Purpose: 2-minute complete health snapshot, paste into incident ticket

SET LINESIZE 200 PAGESIZE 100
SET FEEDBACK OFF
SET ECHO OFF

PROMPT ============================================================
PROMPT ORACLE DATABASE HEALTH SNAPSHOT
PROMPT ============================================================
SELECT 'Database:  ' || name
       || '  Version: ' || version_full
       || '  Uptime: ' || ROUND((SYSDATE - startup_time) * 24, 1) || 'h' AS db_info
FROM v$instance;

PROMPT
PROMPT == DB Time / AAS (last 1 hour) ==
SELECT ROUND(m.value / 1000000 / 60, 1) AS db_time_mins,
       ROUND(m.value / 1000000 / 3600, 2) AS aas
FROM v$sysmetric m
WHERE metric_name = 'DB Time Per Sec'
  AND group_id = 2;

PROMPT
PROMPT == Top 5 Wait Events ==
SELECT event,
       ROUND(time_waited / 100, 1) AS total_wait_secs,
       ROUND(time_waited / total_waits / 100, 2) AS avg_wait_ms
FROM v$system_event
WHERE wait_class != 'Idle'
ORDER BY time_waited DESC
FETCH FIRST 5 ROWS ONLY;

PROMPT
PROMPT == Top 5 SQL by Elapsed Time (AWR, last snapshot) ==
SELECT sql_id,
       ROUND(elapsed_time_delta / 1e6, 1) AS elapsed_secs,
       executions_delta AS execs,
       ROUND(buffer_gets_delta / NULLIF(executions_delta,0)) AS avg_lio
FROM dba_hist_sqlstat
WHERE snap_id = (SELECT MAX(snap_id) - 1 FROM dba_hist_snapshot)
ORDER BY elapsed_time_delta DESC
FETCH FIRST 5 ROWS ONLY;

PROMPT
PROMPT == Memory ==
SELECT ROUND(sga_size / 1024, 0)           AS sga_gb,
       ROUND(pga_aggregate_limit / 1024, 0) AS pga_limit_gb,
       ROUND(pga_aggregate_target / 1024, 0) AS pga_target_gb
FROM v$pgastat
CROSS JOIN (SELECT SUM(value) / 1024 / 1024 AS sga_size FROM v$sga) s
WHERE v$pgastat.name = 'aggregate PGA target parameter';

PROMPT
PROMPT == Active Sessions Right Now ==
SELECT NVL(s.username, '(' || s.type || ')') AS username,
       s.status,
       s.event,
       s.seconds_in_wait,
       SUBSTR(t.sql_text, 1, 60) AS sql_text
FROM v$session s
LEFT JOIN v$sql t ON s.sql_id = t.sql_id AND s.sql_child_number = t.child_number
WHERE s.status = 'ACTIVE'
  AND s.type = 'USER'
ORDER BY s.seconds_in_wait DESC
FETCH FIRST 10 ROWS ONLY;

PROMPT
PROMPT == Redo Log Activity ==
SELECT sequence#, status, bytes/1024/1024 AS size_mb, archived
FROM v$log
ORDER BY sequence#;

PROMPT
PROMPT == Tablespace Usage ==
SELECT t.tablespace_name,
       ROUND(t.total_mb, 0) AS total_mb,
       ROUND(t.total_mb - f.free_mb, 0) AS used_mb,
       ROUND((t.total_mb - f.free_mb) / t.total_mb * 100, 1) AS pct_used
FROM (SELECT tablespace_name, SUM(bytes)/1024/1024 AS total_mb FROM dba_data_files GROUP BY tablespace_name) t
JOIN (SELECT tablespace_name, SUM(bytes)/1024/1024 AS free_mb  FROM dba_free_space     GROUP BY tablespace_name) f
  ON t.tablespace_name = f.tablespace_name
WHERE (t.total_mb - f.free_mb) / t.total_mb > 0.70   -- show only > 70% full
ORDER BY pct_used DESC;

Script Library: Save and Schedule

# [[email protected] ~]
# Schedule the health snapshot to run every 15 minutes and email if issues found
crontab -e

# Add:
# */15 * * * * sqlplus -S / as sysdba @/u01/scripts/quick_health.sql \
#              > /u01/logs/health_$(date +\%H\%M).log 2>&1

# Full AWR analysis — run after each working day
# 30 18 * * 1-5 /u01/scripts/awr_daily_report.sh

These scripts form the foundation of a proactive monitoring programme. Combine them with Oracle Enterprise Manager alerts, OCI Monitoring custom queries, or your observability stack (Prometheus/Grafana with Oracle exporter) for a complete picture.