Oracle tuning with AWR: how to read the report and find the SQL that is costing you a server
A practical guide to diagnosing Oracle Database performance with AWR and ASH: which sections to read first, how to interpret the wait profile and what to do with the top SQL before buying hardware.
TioneSolutions team · October 15, 2026 · 11 min read
When a system on Oracle “gets slow”, the most common reaction is to look at server CPU, see it at 90 % and request a quote for a bigger box. In most of the cases we have handled, that new server would have been wasted: CPU sat at 90 % because a handful of SQL statements performed millions of logical reads they never needed. The AWR report tells you exactly which ones. This guide explains how to read it with judgment.
What AWR is and what you need to use it
The Automatic Workload Repository takes snapshots of instance state every hour by default and keeps them for eight days. An AWR report compares two snapshots and summarizes everything that happened in between: database time, waits, most expensive SQL, I/O per file, memory statistics.
Requirements:
- Oracle Enterprise Edition with the Diagnostics Pack licensed. AWR and ASH belong to that pack. On Standard Edition, or without the pack, use Statspack (free, less detailed) or the
V$views directly. QueryingDBA_HIST_*without a license is a contract violation that Oracle does audit. - Snapshots covering the problem window. If the issue happens from 10:00 to 10:40, a 09:00–11:00 report dilutes the signal. Take manual snapshots with
DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOTbefore and after reproducing the problem.
To generate the report:
-- As SYS or a user with privileges on DBMS_WORKLOAD_REPOSITORY
@?/rdbms/admin/awrrpt.sql
-- Pick html, the snapshot range and the file name
Rule of thumb: a 30-to-60-minute report over the problem window is worth more than a 24-hour one. A daily average hides the peak you care about.
The five sections to read, in order
1. Load Profile: how much work per second?
It is at the top. Compare DB Time(s) per second with DB CPU(s). With 8 cores and DB CPU per second at 7.5, the database is saturating CPU. If DB Time is 40 and DB CPU is 6, the rest of the time the database is waiting for something (I/O, locks, network), not processing.
Also look at Logical reads and Physical reads per second. Hundreds of thousands of logical reads per second with few transactions usually means inefficient plans: full scans where index access belongs, or indexes returning too many rows.
2. Top 10 Foreground Events: what is the database waiting on?
This table is the heart of the report. Each row is a wait class with its share of DB Time. How to read the common ones:
| Event | What it usually means | Where to look next |
|---|---|---|
DB CPU | The database is processing. If it dominates, the problem is CPU-hungry SQL: excessive logical reads, PL/SQL functions in the WHERE, large sorts. | SQL ordered by CPU / Gets |
db file sequential read | Single-block reads, typical of index access. High share plus high average time (>10 ms) points to slow storage. | SQL ordered by Reads, Tablespace IO |
db file scattered read | Multi-block reads: full table scans. If high, queries are sweeping whole tables. | SQL ordered by Reads |
direct path read | Parallel full scans or large tables bypassing the buffer cache. Normal in reporting, suspicious in OLTP. | SQL ordered by Reads |
log file sync | Commits wait for redo to hit disk. Average >5 ms means slow redo disks; a huge count means row-by-row commits in a loop. | Instance Activity (user commits), redo I/O |
enq: TX - row lock contention | Sessions blocked by other sessions. Application design, not infrastructure. | Segments by Row Lock Waits |
buffer busy waits | Several sessions fighting over the same block. Hot insert tables or indexes on sequences. | Segments by Buffer Busy Waits |
library cache lock / cursor: pin S wait on X | Excessive parsing: SQL without bind variables, or DDL at peak time. | Parse statistics, SQL ordered by Parse Calls |
If DB CPU and logical reads dominate, the path is SQL. If I/O waits with high average times dominate, storage may be the cause, but first check whether the I/O is necessary: a bad plan generates I/O that no fast disk will compensate for.
3. SQL ordered by Elapsed Time, CPU, Gets and Reads
This is where “5 % of the SQL burns 80 % of the database” lives. Sort mentally by what dominated the wait events:
- CPU dominated: SQL ordered by Gets (logical reads) and by CPU Time.
- I/O dominated: SQL ordered by Reads (physical reads).
- Users complain about slow screens: SQL ordered by Elapsed Time, and watch
Elapsed per Exec. A statement executed 50,000 times at 0.1 s weighs more than one executed once in 30 s.
Note the SQL Id of the three or four statements leading the lists. They are almost always the same ones across several lists. Then get the plan the database actually used, not the estimate:
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('&sql_id', NULL, 'ALLSTATS LAST'));
-- If the cursor is no longer in memory:
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR('&sql_id'));
What to look for in the plan: TABLE ACCESS FULL on large tables with selective filters, NESTED LOOPS with millions of iterations, HASH JOIN spilling to disk (TempSpc), and large gaps between E-Rows (estimated) and A-Rows (actual), which indicate stale statistics or missing histograms.
4. Segment Statistics: which objects suffer?
Segments by Logical Reads, by Physical Reads, by Row Lock Waits and by Buffer Busy Waits show which tables and indexes carry the load. An index topping logical reads on a small table is usually fragmented or the wrong index for the access pattern.
5. Instance Efficiency and Parse Statistics
A low Execute to Parse % (under 60–70 %) and Soft Parse % below 95 % mean the application sends SQL with literals instead of bind variables. That fills the shared pool, triggers library cache waits and burns CPU on parsing. The fix belongs in the application; in the meantime, CURSOR_SHARING=FORCE is a patch with side effects worth measuring before applying it in production.
ASH: when the problem lasts minutes, not hours
AWR averages an hour. If the slowdown lasts five minutes, ASH (Active Session History) gives you per-second samples of what every active session was doing:
SELECT sql_id, event, COUNT(*) samples
FROM v$active_session_history
WHERE sample_time BETWEEN TO_DATE('2026-10-15 10:05','YYYY-MM-DD HH24:MI')
AND TO_DATE('2026-10-15 10:15','YYYY-MM-DD HH24:MI')
GROUP BY sql_id, event
ORDER BY samples DESC
FETCH FIRST 15 ROWS ONLY;
The ashrpt.sql report does the same with formatting. For a point incident, ASH answers in ten minutes what AWR takes an hour to show.
The quick wins that usually show up
In order of frequency across the diagnostics we have run:
- Stale statistics or missing histograms on tables with skewed distributions. Fix:
DBMS_STATS.GATHER_TABLE_STATSwith a properMETHOD_OPTand, where applicable, incremental statistics on partitioned tables. - Missing index for a highly selective predicate. Before creating it, check how many indexes the table already has: each one makes
INSERT/UPDATEmore expensive. - Unstable plans: the same statement alternates between a good and a bad plan depending on bind values (
bind peeking). Fix:SQL Plan Baselinesto pin the right plan without touching code. - Undersized SGA: low
Buffer Cache Hit %with free memory on the server. CheckBuffer Pool AdvisoryandSGA Target Advisoryin the same report; Oracle tells you how much each increment would gain. - Row-by-row commits in batch jobs.
Instance Activity Statsshowsuser commits; tens of thousands per minute in a nightly job means the change is in the PL/SQL (commit every N rows). - PL/SQL in the
WHERE: a function called for every evaluated row.Function result cacheor a rewrite as aJOINusually cuts the cost by orders of magnitude.
What not to do
- Do not raise parameters at random. Increasing
SGA_TARGETwithout advisory evidence, or settingOPTIMIZER_INDEX_COST_ADJ, can make other plans worse. - Do not reach for hints first. A hint freezes an optimizer decision forever, even as data changes. Prefer correct statistics and baselines.
- Do not restart the instance “to clean up”. You lose the buffer cache, the shared pool and the evidence. If the problem is recurring, it comes back in an hour.
- Do not buy hardware without an AWR report of the problem window showing I/O waits with high times and SQL already optimized. It is the only valid technical justification.
How to measure that it worked
Take the AWR for the same window (same weekday, same hour) before and after the change. Compare total DB Time, the top waits and Elapsed per Exec of the statements you touched. A serious tuning report delivers those three numbers, not an impression that “it feels faster now”.
If your database shows performance symptoms and nobody has looked at an AWR of the problem window, the initial diagnosis takes three to five business days and usually ends with quick wins applied in the first week.
Have a problem, an improvement or a project in mind?
Book a free 30-minute assessment. We talk through your database, applications, cloud or reporting and give you a clear recommendation on where to start.
Book an assessmentMore articles
Oracle to OCI migration checklist: 40 checks before, during and after cutover
A checklist for migrating Oracle databases and application servers from on-premise to Oracle Cloud Infrastructure without surprises: assessment, architecture, migration method, rehearsal, cutover and stabilization.
Modernizing Oracle Forms to APEX: strategy, common mistakes and how to do it without stopping operations
How to migrate an Oracle Forms system to Oracle APEX gradually: inventory, prioritization, what to keep in the database, Forms-APEX coexistence, rewrite patterns and the mistakes that make the project expensive.