How to Create a SQL Monitor Report in Oracle

Monitor a long-running query, find it in V$SQL_MONITOR, and read its SQL Monitor report as text and as an HTML page.

Real-Time SQL Monitoring records what a statement does while it runs, step by step through its execution plan: rows produced, time spent, reads and writes, memory, and temporary space used by each operation. Its report shows where a long statement spends its time far more directly than the plan alone, and it can be produced while the statement is still running. This guide monitors a query that spills a sort to disk, lists it in V$SQL_MONITOR, and creates the report as text and as an HTML page.

Licensing

SQL Monitoring is part of the Oracle Tuning Pack. On Enterprise Edition, use it only when the database is licensed for the Tuning Pack and the parameter CONTROL_MANAGEMENT_PACK_ACCESS includes TUNING. The examples run on Oracle AI Database 26ai Free, as a user with the SELECT_CATALOG_ROLE role.

Syntax

select dbms_sql_monitor.report_sql_monitor(sql_id => 'sql_id', type => 'TEXT')
from   dual;

TYPE can be TEXT, HTML, XML, or ACTIVE. Without a SQL_ID, the report covers the most recently monitored statement.

Sample Tables

Example:

create table demo_customers as
select level as customer_id, 'Customer ' || level as name,
       decode(mod(level, 4), 0, 'India', 1, 'Germany', 2, 'Brazil', 'Japan') as country
from   dual connect by level <= 50000;

create table demo_orders as
select level as order_id, mod(level * 7, 50000) + 1 as customer_id,
       date '2025-01-01' + mod(level, 600) as order_date, mod(level * 13, 500) + 10 as amount
from   dual connect by level <= 2000000;

Output:

Table DEMO_CUSTOMERS created.

Table DEMO_ORDERS created.

Run a Monitored Statement

Oracle monitors a statement automatically once it has used 5 seconds of CPU or I/O time, or when it runs in parallel. The MONITOR hint monitors it from the start. A small sort area makes the sort of 2 million rows spill to the temporary tablespace, which the report will show:

Example:

-- a small sort area, so the sort spills to the temporary tablespace
alter session set workarea_size_policy = manual;
alter session set sort_area_size = 1048576;

-- the three largest orders of every customer
select /*+ monitor */ /* demo_top3 */ count(*) as orders, sum(amount) as total
from  (select o.amount,
              row_number() over (partition by o.customer_id
                                 order by o.amount desc, o.order_date) as rn
       from   demo_orders o
       join   demo_customers c on c.customer_id = o.customer_id)
where  rn <= 3;

Output:

Session altered.

Session altered.

   ORDERS       TOTAL
_________ ___________
   150000    38925000

Find Monitored Statements

V$SQL_MONITOR has one row per monitored execution, while it runs and for a while afterward:

Example:

select sql_id, status, round(elapsed_time / 1e6, 2) as elapsed_s,
       round(cpu_time / 1e6, 2) as cpu_s, buffer_gets
from   v$sql_monitor
where  sql_text like '%demo_top3%'
order  by sql_exec_start desc;

Output:

SQL_ID           STATUS                ELAPSED_S    CPU_S    BUFFER_GETS
________________ __________________ ____________ ________ ______________
g81aruw3qhprt    DONE (ALL ROWS)            1.75     1.54           7919

STATUS shows EXECUTING while the statement runs, and DONE afterward. Without the WHERE clause, the query lists every monitored statement, which is a quick way to find long-running ones.

The Text Report

Example:

set define on long 200000 longchunksize 200000 linesize 200 pagesize 0 heading off
column sql_id new_value mon_sql_id noprint
select sql_id from v$sql_monitor
where  sql_text like '%demo_top3%'
order  by sql_exec_start desc
fetch  first 1 row only;

select dbms_sql_monitor.report_sql_monitor(sql_id => '&mon_sql_id', type => 'TEXT')
from   dual;

Output:

SQL Monitoring Report

SQL Text
------------------------------
select /*+ monitor */ /* demo_top3 */ count(*) as orders, sum(amount) as total from (select o.amount, row_number() over (partition by o.customer_id order by o.amount desc, o.order_date) as rn from dem
o_orders o join demo_customers c on c.customer_id = o.customer_id) where rn <= 3

Global Information
------------------------------
 Status              :  DONE (ALL ROWS)
 Instance ID         :  1
 Session             :  NIMBUS (22:58642)
 SQL ID              :  g81aruw3qhprt
 SQL Execution ID    :  16777216
 Execution Started   :  10/12/2026 04:26:58
 First Refresh Time  :  10/12/2026 04:26:58
 Last Refresh Time   :  10/12/2026 04:27:00
 Duration            :  2s
 Module/Action       :  SQLcl/-
 Service             :  freepdb1
 Program             :  SQLcl
 Fetch Calls         :  1

Global Stats
==============================================================================================
| Elapsed |   Cpu   |    IO    | Application | Fetch | Buffer | Read | Read  | Write | Write |
| Time(s) | Time(s) | Waits(s) |  Waits(s)   | Calls |  Gets  | Reqs | Bytes | Reqs  | Bytes |
==============================================================================================
|    1.79 |    1.54 |     0.25 |        0.00 |     1 |   7919 | 7027 | 189MB |  2863 | 129MB |
==============================================================================================

SQL Plan Monitoring Details (Plan Hash Value=548438158)
===========================================================================================================================================================================================
| Id |          Operation          |      Name      |  Rows   | Cost  |   Time    | Start  | Execs |   Rows   | Read | Read  | Write | Write |  Mem  | Temp  | Activity | Activity Detail |
|    |                             |                | (Estim) |       | Active(s) | Active |       | (Actual) | Reqs | Bytes | Reqs  | Bytes | (Max) | (Max) |   (%)    |   (# samples)   |
===========================================================================================================================================================================================
|  0 | SELECT STATEMENT            |                |         |       |         1 |     +2 |     1 |        1 |      |       |       |       |     . |     . |          |                 |
|  1 |   SORT AGGREGATE            |                |       1 |       |         1 |     +2 |     1 |        1 |      |       |       |       |     . |     . |          |                 |
|  2 |    VIEW                     |                |    152K | 19846 |         1 |     +2 |     1 |     150K |      |       |       |       |     . |     . |          |                 |
|  3 |     WINDOW SORT PUSHED RANK |                |      2M | 19846 |         1 |     +2 |     1 |     150K | 6248 |  96MB |  2169 |  96MB |   1MB |  67MB |          |                 |
|  4 |      HASH JOIN              |                |      2M |  2141 |         2 |     +1 |     1 |       2M |  694 |  33MB |   694 |  33MB |   2MB |  34MB |          |                 |
|  5 |       TABLE ACCESS FULL     | DEMO_CUSTOMERS |   50000 |    64 |         1 |     +2 |     1 |    50000 |   17 |   2MB |       |       |     . |     . |          |                 |
|  6 |       TABLE ACCESS FULL     | DEMO_ORDERS    |      2M |  2074 |         1 |     +2 |     1 |       2M |   68 |  59MB |       |       |     . |     . |          |                 |
===========================================================================================================================================================================================
  • Global Stats shows the totals: 1.75 seconds, almost all of it CPU, 7,919 buffer gets, and the read and write requests.
  • The plan section shows each operation's estimated and actual rows, side by side: 2M estimated and 2M actual for the join, so the estimates are fine.
  • WINDOW SORT PUSHED RANK, the ranking, used 1 MB of memory and 67 MB of temporary space, and did most of the reads and writes: the sort spilled to disk. That is the step to improve, for example with more PGA memory.
  • Activity columns fill with samples for statements that run longer, about one sample per second.

The HTML Report

The HTML version is a self-contained page. SQLcl's SPOOL writes it to a file, which any browser opens:

Example:

set define on long 2000000 longchunksize 2000000 linesize 32767 pagesize 0 heading off trimspool on
column sql_id new_value mon_sql_id noprint
select sql_id from v$sql_monitor
where  sql_text like '%demo_top3%'
order  by sql_exec_start desc
fetch  first 1 row only;

spool /tmp/sqlmon-report.html
select dbms_sql_monitor.report_sql_monitor(sql_id => '&mon_sql_id', type => 'HTML')
from   dual;
spool off

The spooled file, opened in a browser:

Oracle SQL Monitor HTML report showing global information and plan monitoring details with temporary space and IO requests per operation
The HTML report: bars for buffer gets, IO requests, and database time, and per operation the active period, rows, memory, temp, and IO requests.

The bars make the busy step obvious at a glance: the window sort has 85% of the IO requests and 67 MB of temp. The ACTIVE type produces an interactive version of this page, which loads its scripts from Oracle's website when opened.

Things to Know

  • Run the report while a statement is still executing to see which step it is working on and how far it has come.
  • The NO_MONITOR hint excludes a statement from monitoring.
  • Monitoring data is kept in memory and ages out; for statements that ran hours ago, use the report soon or the Automatic Workload Repository, where licensed.

Related Guides

Conclusion

Run long statements with monitoring, automatic after 5 seconds or forced with the MONITOR hint, find them in V$SQL_MONITOR, and create the report with DBMS_SQL_MONITOR.REPORT_SQL_MONITOR. The per-operation time, rows, memory, and temp show exactly which step to tune.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted