Showing BIND values from SQL Monitor views, even when the SQL Monitor report itself does not show it correctly

Hello,

Sometimes, when you have to troubleshoot complex performance issues related to execution plans for queries that use bind variables, knowing the bind values used by the Optimizer becomes very important for the troubleshooting process. And in these cases, not only the bind value used at runtime for a particular execution is important, but also the value used at PARSING time (known as PEEKED value).

So, one wonderful tool to analyze SQL Executions is the SQL Monitor Report. If you don’t know much about SQL Monitor, please read here where Maria Colgan explains very well how to get the most out of this tool.

Well, although SQL Monitor is a very useful tool that I use all the time for performance analysis (I myself have written a few posts about it, find them here), there is something that several times made me sad and lose time: not always the SQL Monitor report shows all bind variables used and/or peeked.

Take for example this SQL Monitor report:

Note that all the PEEKED values as well as the TIMESTAMP datatype value in the Value column (the values used at runtime) are not shown correctly. Actually, the values presented are in RAW format, the way Oracle stores them in the database. Easy to understand that seeing values in the RAW format does not help much right?

I can’t confirm if this is true in all databases and all versions, but all that I have access at this point have the same “issue”. The database where the above report was generated is version 19.20.

So, in this post I will provide you a script that shows all the bind variable values for any generated SQL Monitor report in your database, either if it’s in memory (GV$SQL_MONITOR) or in the AWR repository (persisted in DBA_HIST_REPORTS%).

The script shows both the USED and PEEKED values, for all most common data types including TIMESTAMP. I did not implement it yet for TIMESTAMP WITH TIME ZONE and TIMESTAMP WITH LOCAL TIME ZONE, but it’s not that difficult so I will probably come up with an updated version soon.

As usual, I will provide you a test case so you can test the script even if you don’t have real data to test against.

So, let’s start creating a table and some data:

create table t (
col_n number, col_ch char, col_vc varchar2(10),
col_dt date, col_ts timestamp);

Table created.

define mydt="2023/10/01 12:01:12"
define myts="2023/10/01 12:01:12.123"

insert into t values (
1, 'A', 'Edu$!123', to_date('&mydt','yyyy-mm-dd hh24:mi:ss'), to_timestamp('&myts','yyyy-mm-dd hh24:mi:ss.ff'));

1 row created.

Note that I defined two SQL Plus variables with a DATE and a TIMESTAMP values, just to make it easy to reuse the same values later on for the queries I’m going to execute.

Let me format the output for DATE and TIMESTAMP types and check the inserted data:

alter session set nls_date_format='yyyy-mm-dd hh24:mi:ss';
alter session set nls_timestamp_format='yyyy-mm-dd hh24:mi:ss.ff';
col col_N for 999
col col_ts for a26
select * from t;

Session altered.

Session altered.

COL_N C COL_VC     COL_DT              COL_TS
----- - ---------- ------------------- --------------------------
    1 A Edu$!123   2023-10-01 12:01:12 2023-10-01 12:01:12.123000

1 row selected.

Ok, now I will run a query with bind variables twice: the first execution will parse the query and generate the execution plan with bind values passed; the second one will use different values but use the same plan (with the same peeked values):

set serveroutput on
declare
	n number;
	c char;
	vc varchar2(10);
	dt date;
	ts timestamp;
	function run_query
	return number is
		rn number;
	begin
		select /*+ monitor */ COL_N
		into rn
			from t
			where COL_VC!='MYQUERY'
			and COL_N = n
			and COL_CH = c
			and COL_VC like vc
			and COL_DT = dt
			and COL_TS = ts
			;
		return rn;
	exception when no_data_found then return (-1);
	end;
begin
	n := 1;
	c := 'A';
	vc := 'Edu%';
	dt := to_date('&mydt','yyyy-mm-dd hh24:mi:ss');
	ts := to_timestamp('&myts','yyyy-mm-dd hh24:mi:ss.ff');
	dbms_output.put_line(run_query);
	n := 2;
	c := 'B';
	vc := 'ABC%';
	dt := sysdate;
	ts := systimestamp;	
	dbms_output.put_line(run_query);
end;
/

1
-1

(EDIT 2023/10/10: I just noticed that I included parameters in my function but I never used their values, and it worked because the variables were declared globally so the function have access to them. The code was adjusted).

Note: I am using a PL/SQL block to declare the variables with the proper data type. I am not using SQL Plus bind variables because they cannot be declared directly as DATE or TIMESTAMP.

My PL/SQL block has a function that executes the SQL with the variables received as parameters, and returns the number value in COL_N column, or -1 if no record is found.

After running the PL/SQL block, we will have the two executions stored in the V$SQL_MONITOR view. Please note that I included in the query a string MYQUERY to help identify the query. If you need to rerun the test again you may want to change the string:

col sql_text for a60 trunc
select sql_id, sql_exec_start, sql_exec_id, sql_text
from v$sql_monitor
where sql_text like 'SELECT /*+ monitor */%MYQUERY%';

SQL_ID        SQL_EXEC_START      SQL_EXEC_ID SQL_TEXT
------------- ------------------- ----------- ------------------------------------------------------------
gs8mw9wayq4dm 2023-10-09 15:35:54    16777216 SELECT /*+ monitor */ COL_N FROM T WHERE COL_VC!='MYQUERY' A
gs8mw9wayq4dm 2023-10-09 15:35:54    16777217 SELECT /*+ monitor */ COL_N FROM T WHERE COL_VC!='MYQUERY' A

Run the following query to generate the SQL Monitor for the last execution:

alter session set events = 'emx_control compress_xml=none';
set lines 32767 trimout on trimspool on head off feed off echo off timing off termout off long 1000000 longchunksize 1000000 ver off pages 0
spool sqlmon.html
select DBMS_SQL_MONITOR.report_sql_monitor(
sql_id => max(a.sql_id),
sql_exec_id => max(a.sql_exec_id),
sql_exec_start => max(a.sql_exec_start),
report_level=>'ALL',type=>'ACTIVE')
from v$sql_monitor a
where sql_text like 'SELECT /*+ monitor */%MYQUERY%';
spool off

Then, open the SQL Monitor in a web browser, click on the SQL Text tab and expand the SQL Binds section:

See? Again, all the PEEKED values are presented as RAW, as well as the TIMESTAMPs.

My friend Marcus Vinicius Miguel Pedro found the Doc ID 2111950.1 that points to a Bug, but at least until Oracle v. 19.20 which I am using, the bug is still there. And the note mentions only the TIMESTAMP issue, but not the PEEKED values.

So, here I present the script you can use to properly get all PEEKED and USED values, for data types NUMBER, CHAR, VARCHAR2, DATE and TIMESTAMP.

def sqlid="gs8mw9wayq4dm"
def exec=""
def phv=""
def ins="1"
def datini="sysdate-30"
def datfin="sysdate"

col bind_name format a10 wrap
col used_value format a40
col peeked_value format a40

WITH
reports as (
	select *
	from (
		select snap_id, INSTANCE_NUMBER, r.key1 as sql_id, to_number(r.key2) as sql_exec_id, d.REPORT, r.report_summary,
			to_number(EXTRACTVALUE(XMLType(r.report_summary),'/report_repository_summary/sql/plan_hash')) SQL_PLAN_HASH_VALUE
		from DBA_HIST_REPORTS_DETAILS d 
		natural join DBA_HIST_REPORTS r
		where r.component_name = 'sqlmonitor'
		AND r.report_name = 'main'
		AND KEY1='&sqlid'
		and ('&exec' is null or KEY2 = to_number('&exec'))
		and ('&ins' is null or INSTANCE_NUMBER = to_number(nvl('&ins','1')))
		and GENERATION_TIME between &datini and &datfin
	)
	where ('&phv' is null or SQL_PLAN_HASH_VALUE = to_number('&phv'))
),
reports_u as (
	select
		snap_id, INSTANCE_NUMBER, sql_id, sql_exec_id, bind_name, bind_pos, bind_type, bind_typestr,
 		case
			 when bind_type in (1,2,96) /*varchar2,number,char*/ then bind_data
 			 when bind_type = 12 /*date*/ then to_char(to_date(bind_data,'mm/dd/yyyy hh24:mi:ss'),'yyyy-mm-dd hh24:mi:ss')
 			 when bind_type = 180 /*timestamp*/ then 
					to_char(
						to_timestamp(
							to_char(
								dbms_stats.convert_raw_to_date(hextoraw(substr(bind_data,1,14)))
								,'yyyy-mm-dd hh24:mi:ss')
							,'yyyy-mm-dd hh24:mi:ss') 
							+ numtodsinterval(nvl(to_number(hextoraw(substr(bind_data,15,8)),'XXXXXXXX')/1e9,0), 'SECOND')
						,'yyyy-mm-dd hh24:mi:ss.ff9')
 			 else 'Not printable'
 		end used_value
	from reports r
	cross join xmltable
		(
		'/report/sql_monitor_report/binds/bind' passing xmltype(REPORT)
		columns 
			bind_name varchar2(30) path './@name',
			bind_pos  number path './@pos',
			bind_type number path './@dty',
			bind_typestr varchar2(30) path './@dtystr',
			bind_data varchar2(1000) path '.'
		) c
),
reports_p as (
	select 
		snap_id, INSTANCE_NUMBER, sql_id, sql_exec_id, bind_name, bind_pos, bind_type, bind_typestr,
 		case
 			 when bind_type = 1 /*varchar2*/ then utl_raw.cast_to_varchar2(bind_data)
 			 when bind_type = 2 /*number*/ then to_char(utl_raw.cast_to_number (bind_data))
 			 when bind_type = 96 /*char*/ then to_char(utl_raw.cast_to_varchar2(bind_data))
 			 when bind_type = 12 /*date*/ then TO_CHAR(dbms_stats.convert_raw_to_date(bind_data),'yyyy-mm-dd hh24:mi:ss')
 			 when bind_type = 180 /*timestamp*/ then 
					to_char(
						to_timestamp(
							to_char(
								dbms_stats.convert_raw_to_date(hextoraw(substr(bind_data,1,14)))
								,'yyyy-mm-dd hh24:mi:ss')
							,'yyyy-mm-dd hh24:mi:ss') 
							+ numtodsinterval(nvl(to_number(hextoraw(substr(bind_data,15,8)),'XXXXXXXX')/1e9,0), 'SECOND')
						,'yyyy-mm-dd hh24:mi:ss.ff9')
 			 else 'Not printable'
 		end peeked_value
	from reports r
	cross join xmltable
		(
		'/report/sql_monitor_report/plan/operation/other_xml/peeked_binds/bind' 
		passing xmltype(REPORT)
		columns 
			bind_name varchar2(30) path './@nam',
			bind_pos  number path './@pos',
			bind_type number path './@dty',
			bind_typestr varchar2(30) path './@dtystr',
			bind_data varchar2(1000) path '.'
		) p
),
reports_binds as (
	select snap_id, INSTANCE_NUMBER, sql_id, sql_exec_id, bind_name, bind_pos, 
		u.bind_type, u.bind_typestr,
		u.used_value, p.peeked_value
	from reports_p p
	full join reports_u u using (snap_id, INSTANCE_NUMBER, sql_id, sql_exec_id, bind_name, bind_pos)
	order by bind_pos
),
sqlmon as (
	select INST_ID, sql_id, sql_exec_id, BINDS_XML, SQL_CHILD_ADDRESS, SQL_PLAN_HASH_VALUE
	from gv$sql_monitor m
	where SQL_ID='&sqlid'
	and ('&exec' is null or SQL_EXEC_ID = to_number('&exec'))
	and ('&ins' is null or INST_ID = to_number(nvl('&ins','1')))
	and ('&phv' is null or SQL_PLAN_HASH_VALUE = to_number('&phv'))
	and SQL_PLAN_HASH_VALUE <> 0
	and nvl(PX_QCSID,SID) = SID and nvl(PX_QCINST_ID,INST_ID) = INST_ID --don't show Parallel slaves
	and SQL_EXEC_START between &datini and &datfin
),
sqlmon_u as (
	select
		INST_ID, sql_id, sql_exec_id, bind_name, bind_pos, bind_type, bind_typestr, 
 		case
			 when bind_type in (1,2,96) /*varchar2,number,char*/ then bind_data
 			 when bind_type = 12 /*date*/ then to_char(to_date(bind_data,'mm/dd/yyyy hh24:mi:ss'),'yyyy-mm-dd hh24:mi:ss')
 			 when bind_type = 180 /*timestamp*/ then 
					to_char(
						to_timestamp(
							to_char(
								dbms_stats.convert_raw_to_date(hextoraw(substr(bind_data,1,14)))
								,'yyyy-mm-dd hh24:mi:ss')
							,'yyyy-mm-dd hh24:mi:ss') 
							+ numtodsinterval(nvl(to_number(hextoraw(substr(bind_data,15,8)),'XXXXXXXX')/1e9,0), 'SECOND')
						,'yyyy-mm-dd hh24:mi:ss.ff9')
 			 else 'Not printable'
 		end as used_value
	from sqlmon
	cross join xmltable
		(
			'/binds/bind' passing xmltype(BINDS_XML)
			columns 
				bind_name varchar2(30) path './@name',
				bind_pos  number path './@pos',
				bind_type number path './@dty',
				bind_typestr varchar2(30) path './@dtystr',
				bind_data varchar2(1000) path '.'
		) u
	where BINDS_XML is not null
),
sqlmon_plan_p as (
	select
		m.INST_ID, m.sql_id, m.sql_exec_id, x.bind_name, x.bind_pos, x.bind_type,  
 		case
			 when bind_type = 1 /*varchar2*/ then utl_raw.cast_to_varchar2(bind_data)
 			 when bind_type = 2 /*number*/ then to_char(utl_raw.cast_to_number (bind_data))
 			 when bind_type = 96 /*char*/ then to_char(utl_raw.cast_to_varchar2(bind_data))
 			 when bind_type = 12 /*date*/ then TO_CHAR(dbms_stats.convert_raw_to_date(bind_data),'yyyy-mm-dd hh24:mi:ss')
 			 when bind_type = 180 /*timestamp*/ then 
					to_char(
						to_timestamp(
							to_char(
								dbms_stats.convert_raw_to_date(hextoraw(substr(bind_data,1,14)))
								,'yyyy-mm-dd hh24:mi:ss')
							,'yyyy-mm-dd hh24:mi:ss') 
							+ numtodsinterval(nvl(to_number(hextoraw(substr(bind_data,15,8)),'XXXXXXXX')/1e9,0), 'SECOND')
						,'yyyy-mm-dd hh24:mi:ss.ff9')
 			 else 'Not printable'
 		end peeked_value
	from sqlmon m, gv$sql_plan p,
        xmltable
		(
			'/*/peeked_binds/bind' passing xmltype(p.OTHER_XML)
			columns 
				bind_name varchar2(30) path './@nam',
				bind_pos  number path './@pos',
				bind_type number path './@dty',
				bind_data varchar2(1000) path '.'
		) x
	where p.OTHER_XML is not null
    and m.inst_id = p.inst_id
    and m.sql_id = p.sql_id
    and m.SQL_CHILD_ADDRESS = p.child_address
),
sqlmon_binds as (
	select INST_ID, sql_id, sql_exec_id, bind_name, bind_pos, 
		u.bind_type, u.bind_typestr,
		u.used_value, p.peeked_value
	from sqlmon_plan_p p
	full join sqlmon_u u using (INST_ID, sql_id, sql_exec_id, bind_name, bind_pos)
)
select 'HIST' src, snap_id, instance_number as inst, sql_id, sql_exec_id, 
	bind_name, bind_pos, bind_type, bind_typestr, used_value, peeked_value
from reports_binds
UNION ALL
select 'MEM' src, NULL as snap_id, inst_id as inst, sql_id, sql_exec_id, 
	bind_name, bind_pos, bind_type, bind_typestr, used_value, peeked_value
from sqlmon_binds
order by src, snap_id, inst, sql_id, sql_exec_id, bind_pos, used_value, peeked_value
/

SRC     SNAP_ID       INST SQL_ID        SQL_EXEC_ID BIND_NAME    BIND_POS  BIND_TYPE BIND_TYPESTR                   USED_VALUE                               PEEKED_VALUE
---- ---------- ---------- ------------- ----------- ---------- ---------- ---------- ------------------------------ ---------------------------------------- ----------------------------------------
HIST        214          1 gs8mw9wayq4dm    16777216 :B5                 1          2 NUMBER                         1                                        1
HIST        214          1 gs8mw9wayq4dm    16777216 :B4                 2         96 CHAR(32)                       A                                        A
HIST        214          1 gs8mw9wayq4dm    16777216 :B3                 3          1 VARCHAR2(32)                   Edu%                                     Edu%
HIST        214          1 gs8mw9wayq4dm    16777216 :B2                 4         12 DATE                           2023-10-01 12:01:12                      2023-10-01 12:01:12
HIST        214          1 gs8mw9wayq4dm    16777216 :B1                 5        180 TIMESTAMP                      2023-10-01 12:01:12.123000000            2023-10-01 12:01:12.123000000
HIST        214          1 gs8mw9wayq4dm    16777217 :B5                 1          2 NUMBER                         2                                        1
HIST        214          1 gs8mw9wayq4dm    16777217 :B4                 2         96 CHAR(32)                       B                                        A
HIST        214          1 gs8mw9wayq4dm    16777217 :B3                 3          1 VARCHAR2(32)                   ABC%                                     Edu%
HIST        214          1 gs8mw9wayq4dm    16777217 :B2                 4         12 DATE                           2023-10-09 15:35:54                      2023-10-01 12:01:12
HIST        214          1 gs8mw9wayq4dm    16777217 :B1                 5        180 TIMESTAMP                      2023-10-09 15:35:54.432043000            2023-10-01 12:01:12.123000000
MEM                      1 gs8mw9wayq4dm    16777216 :B5                 1          2 NUMBER                         1                                        1
MEM                      1 gs8mw9wayq4dm    16777216 :B4                 2         96 CHAR(32)                       A                                        A
MEM                      1 gs8mw9wayq4dm    16777216 :B3                 3          1 VARCHAR2(32)                   Edu%                                     Edu%
MEM                      1 gs8mw9wayq4dm    16777216 :B2                 4         12 DATE                           2023-10-01 12:01:12                      2023-10-01 12:01:12
MEM                      1 gs8mw9wayq4dm    16777216 :B1                 5        180 TIMESTAMP                      2023-10-01 12:01:12.123000000            2023-10-01 12:01:12.123000000
MEM                      1 gs8mw9wayq4dm    16777217 :B5                 1          2 NUMBER                         2                                        1
MEM                      1 gs8mw9wayq4dm    16777217 :B4                 2         96 CHAR(32)                       B                                        A
MEM                      1 gs8mw9wayq4dm    16777217 :B3                 3          1 VARCHAR2(32)                   ABC%                                     Edu%
MEM                      1 gs8mw9wayq4dm    16777217 :B2                 4         12 DATE                           2023-10-09 15:35:54                      2023-10-01 12:01:12
MEM                      1 gs8mw9wayq4dm    16777217 :B1                 5        180 TIMESTAMP                      2023-10-09 15:35:54.432043000            2023-10-01 12:01:12.123000000

20 rows selected.

As you can see in the putput above, all the variables were presented with their correct, readable values.

Please note that I have included in the script the following filters you can use:

  • SQL_ID (mandatory)
  • SQL_EXEC_ID
  • PHV (SQL Plan Hash Value)
  • INS (instance)
  • DATINI and DATFIN (for the period to research)

That it for today.

I hope you can find it useful.

EDIT: I forgot to mention some of the sources that helped me to decode the information and build the script: Jonathan Lewis and Bertrand Drouvot, and Tanel Poder. What a Team hum 🙂 ?

See you next time!

1 thought on “Showing BIND values from SQL Monitor views, even when the SQL Monitor report itself does not show it correctly

  1. Pingback: Showing captured bind values from Execution Plans and Captured binds #JoelKallmanDay | EDUARDO CLARO

Leave a comment