Oracle SQL Monitor is the most powerful single-query diagnostic tool in the Oracle performance toolkit. Unlike DBMS_XPLAN.DISPLAY_CURSOR, which gives you a static execution plan, SQL Monitor captures real-time runtime statistics at every step of the execution plan — rows processed, memory used, temporary I/O, CPU time, and elapsed time per operation. For parallel queries, it shows the workload distribution across parallel server processes.

When SQL Monitor Activates Automatically

SQL Monitor begins monitoring a SQL statement automatically when either:

  • The statement runs for more than 5 seconds of CPU or elapsed time (cumulative), or
  • The statement uses parallel execution.

No configuration is required. Monitored statements appear in V$SQL_MONITOR and persist in V$SQL_PLAN_MONITOR for the duration of the SQL_MONITOR retention (controlled by _sqlmon_recycle_time).

-- List recently monitored statements
SELECT sql_id,
       sql_text,
       status,
       elapsed_time / 1000000  AS elapsed_secs,
       cpu_time     / 1000000  AS cpu_secs,
       buffer_gets,
       disk_reads,
       fetches,
       username,
       program
FROM v$sql_monitor
WHERE last_refresh_time >= SYSDATE - 1
ORDER BY elapsed_time DESC
FETCH FIRST 20 ROWS ONLY;

Generating the SQL Monitor Active Report

The richest output is the HTML Active Report — a self-contained interactive web page showing the plan tree, per-operation statistics, and timeline:

-- Generate HTML Active Report (most recent execution)
SELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(
  sql_id     => '&sql_id',
  type       => 'ACTIVE',   -- 'TEXT', 'HTML', or 'ACTIVE'
  report_level => 'ALL'
) AS report
FROM DUAL;

Save the output as a .html file and open it in a browser. The interactive report includes:

  • Gantt-style execution timeline showing when each operation started and finished.
  • Per-operation row counts, actual vs. estimated.
  • Per-operation I/O, memory, and CPU.
  • Parallel distribution map (for parallel queries).

Text Report (for quick terminal analysis)

SELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(
  sql_id       => '&sql_id',
  type         => 'TEXT',
  report_level => 'ALL'
) AS report
FROM DUAL;

Example text output:

SQL Monitoring Report

SQL Text
--------
SELECT /* analytics */ ...

Global Information
------------------------------
 Status              :  DONE
 Instance ID         :  1
 Session             :  SID 145 / Serial# 12984
 SQL ID              :  05tk7cd18wbwu
 SQL Execution ID    :  16777218
 Execution Started   :  03/01/2026 09:14:22
 First Refresh Time  :  03/01/2026 09:14:23
 Last Refresh Time   :  03/01/2026 09:17:45
 Duration            :  203 sec
 Module/Action       :  JDBC Thin Client / analytics_batch

Global Stats
============
| Elapsed | Cpu  | IO    | Fetch  | Buffer | Read  | Read  |
| Time(s) | Time | Waits | Calls  | Gets   | Reqs  | Bytes |
|   203   |  12  | 191   |   1    | 1,240  | 8,902 | 71GB  |

The dramatic gap between CPU time (12 seconds) and elapsed time (203 seconds) with 71 GB of reads immediately points to an I/O-bound full table scan.


Interpreting Per-Operation Statistics

Each plan operation in SQL Monitor shows:

Column Meaning
Rows (Estimated) CBO estimate at parse time
Rows (Actual) Rows actually processed at runtime
Execs How many times this operation was invoked (relevant for nested loops)
Memory Memory used by this operation (sorts, hash joins)
Temp Space Disk spill if memory was exhausted
Read Bytes Physical I/O consumed by this step

Key Diagnostic Patterns

Cardinality explosion: E-rows 100, A-rows 10 million. The optimizer did not expect this many rows — downstream operations (hash joins, sorts) were sized incorrectly, causing memory spills.

-- Find cardinality misestimates from SQL Monitor
SELECT plan_line_id,
       plan_operation || ' ' || plan_options AS operation,
       plan_object_name,
       output_rows AS actual_rows,
       plan_cardinality AS estimated_rows,
       CASE WHEN plan_cardinality > 0
            THEN ROUND(output_rows / plan_cardinality, 1)
            ELSE NULL END AS ratio
FROM v$sql_plan_monitor
WHERE sql_id = '&sql_id'
  AND sql_exec_id = (SELECT MAX(sql_exec_id) FROM v$sql_monitor WHERE sql_id = '&sql_id')
ORDER BY ABS(output_rows - NVL(plan_cardinality,0)) DESC
FETCH FIRST 10 ROWS ONLY;

Memory spill to temp: Temp Space > 0 for a HASH JOIN or SORT ORDER BY operation means the workarea spilled to disk. Fix: increase PGA_AGGREGATE_LIMIT, or investigate why cardinality explosion caused an oversized hash build.

Step duration outlier: Most operations complete in milliseconds but one step takes 3 minutes. This is the bottleneck step — focus analysis there.


SQL Monitor for Parallel Query Diagnosis

SQL Monitor’s parallel breakdown is unique — no other tool provides this visibility.

-- Get parallel slave activity for a specific execution
SELECT plan_line_id,
       process_name,
       output_rows,
       read_bytes / 1024 / 1024 AS read_mb,
       cpu_time   / 1000000     AS cpu_secs,
       elapsed_time / 1000000   AS elapsed_secs
FROM v$sql_plan_monitor
WHERE sql_id    = '&sql_id'
  AND sql_exec_id = '&sql_exec_id'
  AND process_name LIKE 'P%'   -- parallel slaves
ORDER BY plan_line_id, process_name;

Diagnosing Parallel Skew

Parallel skew occurs when one parallel slave processes far more data than others, causing the overall query to wait for the slowest slave. In the SQL Monitor Active Report, this appears as an uneven distribution bar in the parallel section.

Common causes:

  • Hash distribution of partitioned table by a low-cardinality column (all rows with one value go to one slave).
  • Data is heavily skewed (one partition has 80% of the rows; that slave finishes last).

Fix: change the parallel distribution method with a hint or redesign the partitioning key.

-- Force round-robin distribution to avoid skew (at cost of additional data movement)
SELECT /*+ PQ_DISTRIBUTE(t NONE RANDOM) */ *
FROM large_table t
WHERE ...

Forcing SQL Monitor for Short Queries

For short queries that do not automatically trigger SQL Monitor, force it with a hint:

SELECT /*+ MONITOR */ customer_id, SUM(amount)
FROM orders
WHERE order_date >= TRUNC(SYSDATE) - 7
GROUP BY customer_id;

This is useful when reproducing a query in a test environment where the execution time is shorter than the 5-second threshold.


Retaining SQL Monitor History

By default, SQL Monitor reports are retained in memory and cycle out. For post-incident analysis, write the report to a table:

-- Create retention table
CREATE TABLE sql_monitor_archive (
  captured_at DATE DEFAULT SYSDATE,
  sql_id      VARCHAR2(13),
  report      CLOB
);

-- Archive a specific execution
INSERT INTO sql_monitor_archive (sql_id, report)
SELECT '&sql_id',
       DBMS_SQLTUNE.REPORT_SQL_MONITOR(
         sql_id       => '&sql_id',
         type         => 'ACTIVE',
         report_level => 'ALL'
       )
FROM DUAL;

COMMIT;

Automate this via a job that archives SQL Monitor reports for any execution exceeding a threshold:

-- Archive all long-running SQL from the last 30 minutes
INSERT INTO sql_monitor_archive (sql_id, report)
SELECT sql_id,
       DBMS_SQLTUNE.REPORT_SQL_MONITOR(sql_id => sql_id, type => 'ACTIVE')
FROM v$sql_monitor
WHERE elapsed_time > 60000000   -- > 60 seconds
  AND last_refresh_time >= SYSDATE - 30/1440;

COMMIT;

When to Use SQL Monitor vs. DBMS_XPLAN

Scenario Best Tool
Query is currently running V$SQL_MONITOR (real-time)
Long-running query just finished SQL Monitor Active Report
Parallel query diagnosis SQL Monitor (parallel detail)
Fast query (< 5 seconds, no parallel) DBMS_XPLAN.DISPLAY_CURSOR
Historical plan comparison (was the plan different yesterday?) DBMS_XPLAN.DISPLAY_AWR
Batch of SQL statements to tune SQL Tuning Advisor

SQL Monitor and DBMS_XPLAN are complementary. For any query consuming > 60 seconds, always start with SQL Monitor — the per-operation runtime statistics immediately narrow the search space from the entire plan to the one or two operations responsible for the majority of elapsed time.