Need help creating/using variables in Oracle PL/SQL - oracle

I just have a simple code and I hope a simple solution. I just want to create a variable in oracle and use it later on. I'm getting errors about needing a into clause or asking me to enter binds. I've been messing with this for 2 days now. Can someone please explain.
DECLARE
BEGIN_DATE VARCHAR2 (20) := '12/31/2017';
END_DATE VARCHAR2 (20) := '01/01/2019';
BEGIN
SELECT
STATUS
, EQUIPMENT_ID "Eq_ID"
FROM CYNFLEET.V_EQUIPMENT_INFO
WHERE
1=1
AND STATUS Not In ('T','N')
AND SOLD_DATE BETWEEN TO_DATE(:BEGIN_DATE, 'MM/DD/YYYY') AND TO_DATE(:END_DATE, 'MM/DD/YYYY')
AND IN_SERVICE_DATE < TO_DATE(:BEGIN_DATE, 'MM/DD/YYYY') OR STATUS Not In ('T','N')
AND SOLD_DATE Is Null
AND IN_SERVICE_DATE < TO_DATE(:END_DATE, 'MM/DD/YYYY');
END;
Error starting at line : 1 in command -
Error report -
ORA-06550: line 7, column 1:
PLS-00428: an INTO clause is expected in this SELECT statement
06550. 00000 - "line %s, column %s:\n%s"
*Cause: Usually a PL/SQL compilation error.
*Action:

You need stock the result of your selection in a variable with INTO clause.
DECLARE
BEGIN_DATE VARCHAR2 (20) := '12/31/2017';
END_DATE VARCHAR2 (20) := '01/01/2019';
VAR1 varchar2(100);
VAR2 VARCHAR2(100);
BEGIN
SELECT
STATUS
, EQUIPMENT_ID "Eq_ID"
INTO VAR1, VAR2
FROM CYNFLEET.V_EQUIPMENT_INFO
WHERE
1=1
AND STATUS Not In ('T','N')
AND SOLD_DATE BETWEEN TO_DATE(:BEGIN_DATE, 'MM/DD/YYYY') AND TO_DATE(:END_DATE,
'MM/DD/YYYY')
AND IN_SERVICE_DATE < TO_DATE(:BEGIN_DATE, 'MM/DD/YYYY') OR STATUS Not In ('T','N')
AND SOLD_DATE Is Null
AND IN_SERVICE_DATE < TO_DATE(:END_DATE, 'MM/DD/YYYY') AND ROWNUM <= 1;
END;
But this works when the selection returns one column, if you need more columns, your variable must be a table type.

The error you have shown is fairly select-explanatory; as shown in the documentation, you need to select into something - which is what the error says. You will need to declare additional variables to accept the query results; but only if your query returns a single row. If it returns multiple rows you will need to select into a collection, or use a cursor and iterate over those results.
You will get messages about bind variables because... you are using bind variables in your query. You should not prefix local PL/SQL variables with a colon. Bind variables are set by the client, not within the PL/SQL block.
DECLARE
BEGIN_DATE VARCHAR2 (20) := '12/31/2017';
END_DATE VARCHAR2 (20) := '01/01/2019';
BEGIN
FOR rec IN (
SELECT
STATUS
, EQUIPMENT_ID "Eq_ID"
FROM CYNFLEET.V_EQUIPMENT_INFO
WHERE
1=1
AND STATUS Not In ('T','N')
AND SOLD_DATE BETWEEN TO_DATE(BEGIN_DATE, 'MM/DD/YYYY') AND TO_DATE(END_DATE, 'MM/DD/YYYY')
AND IN_SERVICE_DATE < TO_DATE(BEGIN_DATE, 'MM/DD/YYYY') OR STATUS Not In ('T','N')
AND SOLD_DATE Is Null
AND IN_SERVICE_DATE < TO_DATE(END_DATE, 'MM/DD/YYYY')
)
LOOP
-- do some thing with each row's rec.status and rec."Eq_ID"
dbms_output.put_line('Got status ' || rec.status || ' for ' || rec."Eq_ID");
END LOOP;
END;
/
Or with date-type variables and literal values:
DECLARE
BEGIN_DATE DATE := DATE '2017-12-31';
END_DATE DATE := DATE '2019-01-01';
BEGIN
FOR rec IN (
SELECT
STATUS
, EQUIPMENT_ID "Eq_ID"
FROM CYNFLEET.V_EQUIPMENT_INFO
WHERE
1=1
AND STATUS Not In ('T','N')
AND SOLD_DATE BETWEEN BEGIN_DATE AND END_DATE
AND IN_SERVICE_DATE < BEGIN_DATE OR STATUS Not In ('T','N')
AND SOLD_DATE Is Null
AND IN_SERVICE_DATE < END_DATE
)
LOOP
-- do some thing with each row's rec.status and rec."Eq_ID"
dbms_output.put_line('Got status ' || rec.status || ' for ' || rec."Eq_ID");
END LOOP;
END;
/
You need to be careful as between is inclusive; given your values you might actually want:
AND SOLD_DATE > BEGIN_DATE
AND SOLD_DATE < END_DATE
though it's hard to tell. As dates include times it would be more common on set your begin date to the first date you want information from (not, as it appears, the day before) and then use SOLD_DATE >= BEGIN_DATE.
You also need to be careful about operator precedence and evaluation order, as you are mixing and and or. It's better to be explicit about which bits of logic are tied together; it looks like you might want:
AND STATUS Not In ('T','N')
AND (
(
SOLD_DATE > BEGIN_DATE
AND SOLD_DATE < END_DATE
AND IN_SERVICE_DATE < BEGIN_DATE
)
OR
(
SOLD_DATE Is Null
AND IN_SERVICE_DATE < END_DATE
)
)
If you did actually want to use bind variables and just display the output then you would set those in your client and use plain SQL; e.g. in SQL*Plus, SQL Developer, SQLcl and possibly others:
-- client variables
var BEGIN_DATE VARCHAR2(10);
var END_DATE VARCHAR2(10);
-- set variables via a PL/SQL block
BEGIN
BEGIN_DATE := '2017-12-31';
END_DATE := '2019-01-01';
END;
/
-- plain SQL query
SELECT
STATUS
, EQUIPMENT_ID "Eq_ID"
FROM CYNFLEET.V_EQUIPMENT_INFO
WHERE
1=1
AND STATUS Not In ('T','N')
AND STATUS Not In ('T','N')
AND (
(
SOLD_DATE > TO_DATE(:BEGIN_DATE, 'YYYY-MM-DD')
AND SOLD_DATE < TO_DATE(:END_DATE, 'YYYY-MM-DD')
AND IN_SERVICE_DATE < TO_DATE(:BEGIN_DATE, 'YYYY-MM-DD')
)
OR
(
SOLD_DATE Is Null
AND IN_SERVICE_DATE < TO_DATE(:END_DATE, 'YYYY-MM-DD')
)
);
Now those variable references in the query are actually bind variables, so they do have the colon prefix.

You declare and use variables like this:
declare
someVar varchar2(20) := 'Hello, world!';
begin
dbms_output.put_line(someVar);
end;
You populate variables from a query using
select a, b, c into x, y, z
from ...
where x, y and z are variables.

Related

Can execute immediate be used with JSON_TABLE

I need to convert JSON into data table (key value columns) in Oracle 12c v12.1.0.2
So for example there is a JSON string like
{"ID": 10, "Description": "TestJSON", "status":"New"}
I need this converted to :
Column1 Column2
------------------------------------
ID 10
Description TestJSON
status New
Now my JSON string could change the number of attributes and hence I require to keep the conversion dynamic.
I tried using execute immediate :
set serveroutput on;
declare
sqlsmt VARCHAR2(200);
t3 varchar2(50);
begin
sqlsmt := 'SELECT * '||
'FROM json_table( ( select jsonstr from mytable where ID= 10) , ''$[*]'' '||
'COLUMNS ( :t1 PATH ''$.''|| '':t2'' ))';
execute immediate sqlsmt into t3 using 'desc' , '$.Description' ;
DBMS_OUTPUT.PUT_LINE( 'Output Variable: ' || t3);
END;
However, I get the following error:
ORA-00904: : invalid identifier
ORA-06512: at line 8
00904. 00000 - "%s: invalid identifier"
Please help. I have Oracle 12c V1. But I really need to pull columns dynamically from JSON.
There are a couple of things that can help with dynamic SQL (assuming you really need to use it). The first is to use dbms_output to show the generated statement before you try to execute it; so in your case:
...
dbms_output.put_line(sqlsmt);
execute immediate sqlsmt into t3;
--using 'descr' , '$.Description' ;
DBMS_OUTPUT.PUT_LINE( 'Output Variable: ' || t3);
END;
/
with your code that shows:
SELECT * FROM json_table( ( select jsonstr from mytable where ID= 10) , '$[*]' COLUMNS ( :t1 PATH '$.'|| ':t2' ))
The most obvious issue there is in '$.'|| ':t2', where :t2 shouldn't be in quotes; that isn't causing the error but would stop it being bound to your variable as you expect as it's a literal value. You also have the $. part in that bit and in your variable value, but again it isn't getting that far.
In common with all dynamic SQL, you can only supply values for variables in the using clause. You're trying to pass the column name as a bind variable, which isn't allowed; so it's trying to use :t1 as the output column name, not desc; and :t1 isn't a valid name. (Neither is desc as that's a reserved word - but either gets the same error.) So, you have to concatenate the column name in rather than binding it.
It looks like you would be able to use :t2 for the path though; but you you can't do that either, not as a dynamic SQL restriction but as a SQL/JSON one - if you got that far, with a valid variable value, you'd still get "ORA-40454: path expression not a literal". You have to concatenate the path into the statement too.
Finally the $[*] doesn't allow you to match the Description... which leads to the second hint about dynamic SQL; get a static query working properly first, then make that dynamic.
So putting that together, you could do:
declare
sqlsmt varchar2(200);
t1 varchar2(30) := 'descr';
t2 varchar2(30) := 'Description';
t3 varchar2(50);
begin
sqlsmt := 'SELECT * '||
'FROM json_table( ( select jsonstr from mytable where ID= 10) , ''$'' '||
'COLUMNS ( ' || t1 || ' PATH ''$.' || t2 || '''))';
dbms_output.put_line(sqlsmt);
execute immediate sqlsmt into t3;
dbms_output.put_line( 'Output Variable: ' || t3);
end;
/
which with your example data outputs:
SELECT * FROM json_table( ( select jsonstr from mytable where ID= 10) , '$' COLUMNS ( descr PATH '$.Description'))
Output Variable: TestJSON
It's a bit odd that the only thing you are allowed to pass as a variable, the 10, is hard-coded. But I get this is an experiment.
You could also write the statement as:
select j.*
from mytable t
cross join json_table ( t.jsonstr, '$' columns ( descr path '$.Description' )) j
where t.id = 10;
which you could do dynamically as:
declare
sqlsmt varchar2(200);
id number := 10;
t1 varchar2(30) := 'descr';
t2 varchar2(30) := 'Description';
t3 varchar2(50);
begin
sqlsmt := 'select j.*'
|| ' from mytable t'
|| q'^ cross join json_table ( t.jsonstr, '$' columns ( ^'
|| t1
|| q'^ path '$.^'
|| t2
|| q'^' )) j^'
|| ' where t.id = :id';
dbms_output.put_line(sqlsmt);
execute immediate sqlsmt into t3 using id;
dbms_output.put_line( 'Output Variable: ' || t3);
end;
/
I've used the alternative quoting mechanism to avoid having to double-up the quotes within the statement, but that's optional. With the same data that outputs:
select j.* from mytable t cross join json_table ( t.jsonstr, '$' columns ( descr path '$.Description' )) j where t.id = :id
Output Variable: TestJSON
db<>fiddle

How to return a record set from a Function in Oracle

I am trying to get the record data using function by passing values
please find the below
CREATE TABLE "TEST"
( "TEST_ID" NUMBER(9,0) NOT NULL ENABLE,
"TEST_DESC" VARCHAR2(30 BYTE),
"TEST_DATE" DATE
);
create or replace TYPE TEST_OBJ_TYPE IS OBJECT
(
TEST_ID NUMBER(9),
TEST_DESC VARCHAR(30),
dates date
);
create or replace TYPE TEST_TABTYPE AS TABLE OF TEST_OBJ_TYPE;
Using the above object and table type created the function as follows
create or replace FUNCTION GET_ROWS(dates date)RETURN TEST_TABTYPE
AS
V_Test_Tabtype Test_TabType;
table_name varchar2(30);
q1 varchar2(300);
BEGIN
table_name :='Test';
q1 := 'SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC)FROM' || '
(SELECT TEST_ID, TEST_DESC FROM ' || table_name || ' where
TEST_DATE = '''||dates||''' ) A';
dbms_output.put_line(q1);
EXECUTE IMMEDIATE q1 BULK COLLECT INTO V_Test_TabType ;
RETURN V_Test_TabType;
EXCEPTION
WHEN OTHERS THEN
v_Test_TabType.DELETE;
RETURN v_Test_TabType;
END;
When I execute this the SQL is printing correctly but not giving the record value.
Error as follows:
select (GET_ROWS('01-08-18')) from dual
Error report -
ORA-02315: incorrect number of arguments for default constructor
ORA-06512: at "AMTEL_MIS.GET_ROWS", line 13
SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC) FROM (SELECT TEST_ID, TEST_DESC FROM Test where TEST_DATE = '01-08-18' ) A
Please assist me further
Thanks in advance
Your type TEST_OBJ_TYPE is defined with three attributes: TEST_ID, TEST_DESC, DATES. However, your query populates the constructor with just two columns:
SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC) FROM
You're missing a value for DATES and that's why Oracle hurls ORA-02315.
I have tried as per your suggestion but it's is giving me an error
ORA-00904: "A"."DATES": invalid identifier
Because of the convoluted way your function is written you need to include TEST_DATE (or dates) in both the subquery and the object constructor:
q1 := 'SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC,A.TEST_DATE)FROM' || ' -- here!
(SELECT TEST_ID, TEST_DESC, TEST_DATE FROM ' -- and here!
|| table_name || ' where TEST_DATE = '''||dates||''' ) A';
If you do that your code will work. Here is a LiveSQL demo of your code with the fix. (Free Oracle login required).
As it seems likely that you will want to pass in the table name so here is a version of your code which does that:
create or replace function get_rows(dates date, p_table_name in varchar2)
return test_tabtype
as
v_test_tabtype test_tabtype;
q1 varchar2(300);
begin
q1 := 'select test_obj_type(a.test_id, a.test_desc,a.test_date) from'
|| '(select test_id, test_desc, test_date from '
|| p_table_name
|| ' where test_date = :1 ) a';
dbms_output.put_line(q1);
execute immediate q1
bulk collect into v_test_tabtype
using dates ;
return v_test_tabtype;
exception
when others then
v_test_tabtype.delete;
return v_test_tabtype;
end;
Note how much easier it is to understand the code when it is laid out with consistent use of case and regular indentation. Readability is a feature!

PL SQL 'IF or CASE' using variable date

I am stuck with this and could use advice/help:
Basically, trying to set the date as a variable and then run select statements, using that date variable in the 'WHERE' section of the query. Not sure if I should be using IF or CASE, or neither? If its monday, i want to run 1 set of dates (prev thur and fri) any other day (just sysdate-2 and sysdate-1) Any help is much appreciated!
Code is below:
DECLARE
today_date number;
start_date date;
end_date date;
BEGIN
today_date := to_char(sysdate, 'D');
start_date := case when today_date ='2' then 'sysdate-4' else 'sysdate-2'
end;
end_date := case when today_date ='2' then 'sysdate-3' else 'sysdate-1' end;
SELECT COLUMN A, COLUMN B, COLUMN C, COLUMN D
FROM /*csv*/REPORT_NAME
WHERE COLUMN B between trunc(start_date)+21/24 and trunc(end_date)+21/24 and
BOOK_NAME = 'xxxxxx' and SERVER = 'xxxxxx' and EX_ACTION = 'xxxxx';
end;
You're mixing variables/functions and strings. This should work.
DECLARE
today_date number;
start_date date;
end_date date;
BEGIN
today_date := to_char(sysdate, 'D');
start_date := case when today_date ='2' then sysdate-4 else sysdate-2
end;
end_date := case when today_date ='2' then sysdate-3 else sysdate-1 end;
/* this won't work without declaring a cursor, and returning it to the client
SELECT COLUMN A, COLUMN B, COLUMN C, COLUMN D
FROM REPORT_NAME
WHERE COLUMN B between trunc(start_date)+21/24 and trunc(end_date)+21/24 and
BOOK_NAME = 'xxxxxx' and SERVER = 'xxxxxx' and EX_ACTION = 'xxxxx';
*/
end;
Note you also have some implicit type conversion happening. today_date should probably be char(1) instead.
You don't really need variables for this, and you don't need PL/SQL; you can calculate the dates as part of the where clause using case expressions in that instead:
SELECT /*csv*/ COLUMN_A, COLUMN_B, COLUMN_C, COLUMN_D
FROM REPORT_NAME
WHERE COLUMN_B >= trunc(sysdate) - case to_char(sysdate, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH')
when 'Mon' then 4 else 2 end + 21/24
AND COLUMN_B < trunc(sysdate) - case to_char(sysdate, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH')
when 'Mon' then 3 else 1 end + 21/24
AND BOOK_NAME = 'xxxxxx'
AND SERVER = 'xxxxxx'
AND EX_ACTION = 'xxxxx';
I've taken the liberty of changing the logic from being based on the day number being 2 to it having a specific day abbreviation, because the value D returns is based on your NLS settings so it could vary (and give unexpected results) for someone else running your code. As day names and abbreviations are also NLS-dependent I've specified the language to use. (You can't specify how D is used in the same way, unfortunately).
I've changed the time window slightly, so it goes from 9pm on one day to up to but not including 9pm the next day. If you use between then it's inclusive at both ends, so runs on consecutive days you both pick up any data at exactly 21:00:00 from the overlapping day. That probably isn't what you want (but if it really is, just change < to <=, or revert to between if you prefer...).
If you want a PL/SQL wrapper you can do the same thing, but you either have to select the results into something like a collection, or use a ref cursor to return the result set to the caller. It isn't clear if you actually need or want to do that though.
have you tried selecting into your variables?
DECLARE
TODAY_DATE NUMBER;
START_DATE DATE;
END_DATE DATE;
BEGIN
TODAY_DATE := TO_CHAR (SYSDATE, 'D');
SELECT CASE WHEN TODAY_DATE = '2' THEN SYSDATE - 4 ELSE SYSDATE - 2 END,
CASE WHEN TODAY_DATE = '2' THEN SYSDATE - 3 ELSE SYSDATE - 1 END
INTO START_DATE, END_DATE
FROM DUAL;
SELECT COLUMN_A, COLUMN_B, COLUMN_C, COLUMN_D
FROM REPORT_NAME /*csv*/
WHERE COLUMN_B BETWEEN TRUNC (START_DATE) + 21 / 24
AND TRUNC (END_DATE) + 21 / 24
AND BOOK_NAME = 'xxxxxx'
AND SERVER = 'xxxxxx'
AND EX_ACTION = 'xxxxx';
END;

ORA-01861: literal does not match format string.while executing procedure

I have written procedure which have date paramters defined as below:
in_Spendpaidstartdt IN DATE,
in_Spendpaidenddt IN DATE,
while with in procedure i am calling these paramters as:
AND ( in_Spendpaidstartdt IS NULL
OR err.Spendpaiddt >= in_Spendpaidstartdt)
AND ( in_Spendpaidenddt IS NULL
OR err.Spendpaiddt <= in_Spendpaidenddt));
however oracle is giving following error:
"ORA-01861: literal does not match format string"
Some one please suggest the work around.
Here is dummy:
CREATE OR REPLACE PROCEDURE XYZ (
in_startdt IN DATE,
in_enddt IN DATE,
output OUT SYS_REFCURSOR)
IS
rcrdnums VARCHAR2 (32767);
rcrd_cnt INT;
BEGIN
rcrd_cnt := 500;
SELECT RTRIM (
XMLCAST (
XMLAGG (XMLELEMENT (e, RCRDNUM) ORDER BY RCRDNUM) AS CLOB),
',')
INTO rcrdnums
FROM (SELECT (ERR.RCRDNUM || ',') AS RCRDNUM
FROM Table_NAME ERR
WHERE ROWNUM <= rcrd_cnt
and ( in_startdt IS NULL
OR to_date(err.paiddt, 'dd/mm/yyyy') >= to_date(in_startdt, 'dd/mm/yyyy'))
AND ( in_enddt IS NULL
OR to_date(err.paiddt, 'dd/mm/yyyy') <= to_date(in_enddt, 'dd/mm/yyyy')));
IF LENGTH (rcrdnums) = 1
THEN
rcrdnums := NULL;
ELSE
rcrdnums := rcrdnums;
--SUBSTR (rcrdnums, 1, LENGTH (rcrdnums) - 1);
END IF;
DBMS_OUTPUT.PUT_LINE (rcrdnums);
OPEN outputFOR
SELECT *
FROM Table_NAME ERR
INNER JOIN ( SELECT REGEXP_SUBSTR (rcrdnums,
'[^,]+',
1,
LEVEL)
AS EVENT
FROM DUAL
CONNECT BY REGEXP_SUBSTR (rcrdnums,
'[^,]+',
1,
LEVEL)
IS NOT NULL) EVENT_P
ON EVENT_P.EVENT = ERR.RCRDNUM;
END;
/
As already mentioned to #XING, your issues are two-fold.
You are forcing Oracle to do an implicit conversion of a DATE back to a string, when you used to_date on something that's already a DATE - something I've already mentioned elsewhere on stackoverflow!
You are (probably) not passing in the parameters correctly when calling your procedure.
Here is how I'd amend your procedure:
CREATE OR REPLACE PROCEDURE XYZ (
in_startdt IN DATE,
in_enddt IN DATE,
output OUT SYS_REFCURSOR)
IS
rcrdnums VARCHAR2 (32767);
rcrd_cnt INT;
BEGIN
rcrd_cnt := 500;
SELECT RTRIM (
XMLCAST (
XMLAGG (XMLELEMENT (e, RCRDNUM) ORDER BY RCRDNUM) AS CLOB),
',')
INTO rcrdnums
FROM (SELECT (ERR.RCRDNUM || ',') AS RCRDNUM
FROM Table_NAME ERR
WHERE ROWNUM <= rcrd_cnt
and ( in_startdt IS NULL
OR to_date(err.paiddt, 'dd/mm/yyyy') >= in_startdt) -- in_startdt is already a DATE, so no need to convert it
AND ( in_enddt IS NULL
OR to_date(err.paiddt, 'dd/mm/yyyy') <= in_enddt)); -- in_enddt is already a DATE, so no need to convert it
IF LENGTH (rcrdnums) = 1
THEN
rcrdnums := NULL;
ELSE
rcrdnums := rcrdnums;
--SUBSTR (rcrdnums, 1, LENGTH (rcrdnums) - 1);
END IF;
DBMS_OUTPUT.PUT_LINE (rcrdnums);
OPEN output FOR
SELECT *
FROM Table_NAME ERR
INNER JOIN ( SELECT REGEXP_SUBSTR (rcrdnums,
'[^,]+',
1,
LEVEL)
AS EVENT
FROM DUAL
CONNECT BY REGEXP_SUBSTR (rcrdnums,
'[^,]+',
1,
LEVEL)
IS NOT NULL) EVENT_P
ON EVENT_P.EVENT = ERR.RCRDNUM;
END;
/
And to test, I'd call your procedure like so:
declare
v_refcur sys_refcursor;
begin
xyz(in_startdt => to_date('01/10/2016', 'dd/mm/yyyy'),
in_enddt => to_date('05/10/2016', 'dd/mm/yyyy'),
output => v_refcur);
end;
/
N.B. it's bad practice to use "select *" in production code - you should explicitly specify the columns you're wanting to get back; that way, if someone adds a column, your code won't cause something to break because it won't pass that extra column along.

Variable/Literal replacement for PL/SQL Cursors?

I often have to debug cursors in Oracle PL/SQL. My problem is that I end up with a few hundered lines big cursors with like 50+ variables and constants. I'm searching for a way to get a version of the statement where constants and variables are replaced with their literals. If I want to find out why the cursor isn't showing the record/line it should I end up replacing those variables/literals for 30 minutes before I can run the select and comment out some of the statements to find out what's wrong.
So if I have something like
CURSOR cFunnyCursor (
v1 NUMBER,
v2 NUMBER
) IS
SELECT * FROM TABLE
WHERE col1 = v1
AND col2 != v2
AND col3 = CONSTANT;
I need the SELECT like this:
SELECT * FROM TABLE
WHERE col1 = 123
AND col2 != 5324
AND col3 = 'ValueXyz';
is there any way to get/log the SELECT in that way so I could just copy paste it in a new SQL window so I don't have to spend 30 minutes to replace that stuff? (should be something I can reuse that's not bind to that special cursor because I need that stuff quite often on a ton of different cursors).
The below function replaces bind variables with recent literals, using data from GV$SQL_BIND_CAPTURE. Oracle bind metadata is not always available, so the below function may not work with all queries.
Create the function:
create or replace function get_sql_with_literals(p_sql_id varchar2) return clob authid current_user is
/*
Purpose: Generate a SQL statement with literals, based on values in GV$SQL_BIND_CAPTURE.
This can be helpful for queries with hundreds of bind variables (or cursor sharing),
and you don't want to spend minutes manually typing each variable.
*/
v_sql_text clob;
v_names sys.odcivarchar2list;
v_values sys.odcivarchar2list;
begin
--Get the SQL_ID and text.
--(Use dynamic SQL to simplify privileges. Your user must have access to GV$ views,
-- but you don't need to have them directly granted to your user, role access is fine.)
execute immediate
q'[
select sql_fulltext
from gv$sql
--There may be multiple rows, for clusters or child cursors.
--Can't use distinct with CLOB SQL_FULLTEXT, but since the values will be the same
--we can pick any one of the rows.
where sql_id = :p_sql_id
and rownum = 1
]'
into v_sql_text
using p_sql_id;
--Try to find the binds from GV$SQL_MONITOR. If the values exist, this is the most accurate source.
execute immediate
q'[
--Get the binds for the latest run.
select
case
when name like ':SYS_%' then ':"' || substr(name, 2) || '"'
else name
end name,
case
when dtystr like 'NUMBER%' then nvl(the_value, 'NULL')
when dtystr like 'VARCHAR2%' then '''' || the_value || ''''
when dtystr like 'DATE%' then 'to_date('''||the_value||''', ''MM/DD/YYYY HH24:MI:SS'')'
--From: https://ardentperf.com/2013/11/19/convert-rawhex-to-timestamp/
when dtystr like 'TIMESTAMP%' then
'to_timestamp('''||
to_char( to_number( substr( the_value, 1, 2 ), 'xx' ) - 100, 'fm00' ) ||
to_char( to_number( substr( the_value, 3, 2 ), 'xx' ) - 100, 'fm00' ) ||
to_char( to_number( substr( the_value, 5, 2 ), 'xx' ), 'fm00' ) ||
to_char( to_number( substr( the_value, 7, 2 ), 'xx' ), 'fm00' ) ||
to_char( to_number( substr( the_value, 9, 2 ), 'xx' )-1, 'fm00' ) ||
to_char( to_number( substr( the_value,11, 2 ), 'xx' )-1, 'fm00' ) ||
to_char( to_number( substr( the_value,13, 2 ), 'xx' )-1, 'fm00' ) ||
''', ''yyyymmddhh24miss'')'
else 'Unknown type: '||dtystr
end the_value
from
(
select xmltype.createXML(binds_xml) binds_xml
from
(
select binds_xml, last_refresh_time, max(last_refresh_time) over () max_last_refresh_time
from gv$sql_monitor
where sql_id = :p_sql_id
and binds_xml is not null
)
where last_refresh_time = max_last_refresh_time
and rownum = 1
) binds
cross join
xmltable('/binds/bind' passing binds.binds_xml
columns
name varchar2(128) path '#name',
dtystr varchar2(128) path '#dtystr',
the_value varchar2(4000) path '/'
)
--Match longest names first to avoid matching substrings.
--For example, we don't want ":b1" to be matched to ":b10".
order by length(name) desc, the_value
]'
bulk collect into v_names, v_values
using p_sql_id;
--Use gv$sql_bind_capture if there was nothing from SQL Monitor.
if v_names is null or v_names.count = 0 then
--Get bind data.
execute immediate
q'[
select
name,
--Convert to literals that can be plugged in.
case
when datatype_string like 'NUMBER%' then nvl(value_string, 'NULL')
when datatype_string like 'VARCHAR%' then '''' || value_string || ''''
when datatype_string like 'DATE%' then 'to_date('''||value_string||''', ''MM/DD/YYYY HH24:MI:SS'')'
--TODO: Add more types here
end value
from
(
select
datatype_string,
--If CURSOR_SHARING=FORCE, literals are replaced with bind variables and use a different format.
--The name is stored as :SYS_B_01, but the actual string will be :"SYS_B_01".
case
when name like ':SYS_%' then ':"' || substr(name, 2) || '"'
else name
end name,
position,
value_string,
--If there are multiple bind values captured, only get the latest set.
row_number() over (partition by name order by last_captured desc nulls last, address) last_when_1
from gv$sql_bind_capture
where sql_id = :p_sql_id
)
where last_when_1 = 1
--Match longest names first to avoid matching substrings.
--For example, we don't want ":b1" to be matched to ":b10".
order by length(name) desc, position
]'
bulk collect into v_names, v_values
using p_sql_id;
end if;
--Loop through the binds and replace them.
for i in 1 .. v_names.count loop
v_sql_text := replace(v_sql_text, v_names(i), v_values(i));
end loop;
--Return the SQL.
return v_sql_text;
end;
/
Run the function:
Oracle only captures the first instance of bind variables. Run this statement before running the procedure to clear existing bind data. Be careful running this statement in production, it may temporarily slow down the system because it lost cached plans.
alter system flush shared_pool;
Now find the SQL_ID. This can be tricky, depending on how generic or unique the SQL is.
select *
from gv$sql
where lower(sql_fulltext) like lower('%unique_string%')
and sql_fulltext not like '%quine%';
Finally, plug the SQL into the procedure and it should return the code with literals. Unfortunately the SQL lost all formatting. There's no easy way around this. If it's a huge deal you could potentially build something using PL/Scope to replace the variables in the procedure instead but I have a feeling that would be ridiculously complicated. Hopefully your IDE has a code beautifier.
select get_sql_with_literals(p_sql_id => '65xzbdjubzdqz') sql
from dual;
Full example with a procedure:
I modified your source code and added unique identifiers so the queries can be easily found. I used a hint because parsed queries do not include regular comments. I also changed the data types to include strings and dates to make the example more realistic.
drop table test1 purge;
create table test1(col1 number, col2 varchar2(100), col3 date);
create or replace procedure test_procedure is
C_Constant constant date := date '2000-01-01';
v_output1 number;
v_output2 varchar2(100);
v_output3 date;
CURSOR cFunnyCursor (
v1 NUMBER,
v2 VARCHAR2
) IS
SELECT /*+ unique_string_1 */ * FROM TEST1
WHERE col1 = v1
AND col2 != v2
AND col3 = C_CONSTANT;
begin
open cFunnyCursor(3, 'asdf');
fetch cFunnyCursor into v_output1, v_output2, v_output3;
close cFunnyCursor;
end;
/
begin
test_procedure;
end;
/
select *
from gv$sql
where lower(sql_fulltext) like lower('%unique_string%')
and sql_fulltext not like '%quine%';
Results:
select get_sql_with_literals(p_sql_id => '65xzbdjubzdqz') sql
from dual;
SQL
---
SELECT /*+ unique_string_1 */ * FROM TEST1 WHERE COL1 = 3 AND COL2 != 'asdf' AND COL3 = to_date('01/01/2000 00:00:00', 'MM/DD/YYYY HH24:MI:SS')
The way I do this is to copy and paste the sql into an editor window, prepend all the variables with : and then run the query. As I use Toad, I get a window prompting me for values for all the bind variables in the query, so I fill those out and the query runs. Values are saved, so the query can be rerun without much hassle, or if you need to tweak a value, you can do.
e.g.:
SELECT * FROM TABLE
WHERE col1 = v1
AND col2 != v2
AND col3 = CONSTANT;
becomes
SELECT * FROM TABLE
WHERE col1 = :v1
AND col2 != :v2
AND col3 = :CONSTANT;
I think you have to use Dynamic SQL functionality to get those variable values. By using ref cursor variable you can even see the output.
Please take a look at the below query.
DECLARE
vString VARCHAR2 (32000);
vResult sys_refcursor;
BEGIN
vString :=
'SELECT * FROM table
WHERE col1 = '|| v1|| '
AND col2 != '|| v2|| '
AND col3 = '|| v;
OPEN vResult FOR vString;
DBMS_OUTPUT.put_line (vString);
END;
If you have a larger Cursor query it is not a efficient way. Because you may need to replace whole Cursor query into Dynamic SQL.
A possible approach would be assiging the cursor to a SYS_REFCURSOR variable, and then assign the SYS_REFCURSOR to a bind variable.
If you run this snippet in Toad, you'll be asked to define the :out variable in the pop-up window: just select Direction: OUT / Type: CURSOR and the dataset will be shown in the "Data Grid" tab.
declare
l_refcur sys_refcursor;
v1 varchar2(4) := 'v1';
v2 varchar2(4) := 'v2';
c_constant varchar2(4) := 'X';
begin
open l_refcur for
SELECT * FROM dual
WHERE dummy = c_CONSTANT;
:out := l_refcur;
end;
Other SQL IDEs should support this feature as well.

Resources