Variable/Literal replacement for PL/SQL Cursors? - oracle

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.

Related

Change row to column in query select Oracle with column many different date [duplicate]

... pivot (sum(A) for B in (X))
Now B is of datatype varchar2 and X is a string of varchar2 values separated by commas.
Values for X are select distinct values from a column(say CL) of same table. This way pivot query was working.
But the problem is that whenever there is a new value in column CL I have to manually add that to the string X.
I tried replacing X with select distinct values from CL. But query is not running.
The reason I felt was due to the fact that for replacing X we need values separated by commas.
Then i created a function to return exact output to match with string X. But query still doesn't run.
The error messages shown are like "missing righr parantheses", "end of file communication channel" etc etc.
I tried pivot xml instead of just pivot, the query runs but gives vlaues like oraxxx etc which are no values at all.
Maybe I am not using it properly.
Can you tell me some method to create a pivot with dynamic values?
You cannot put a dynamic statement in the PIVOT's IN statement without using PIVOT XML, which outputs some less than desirable output. However, you can create an IN string and input it into your statement.
First, here is my sample table;
myNumber myValue myLetter
---------- ---------- --------
1 2 A
1 4 B
2 6 C
2 8 A
2 10 B
3 12 C
3 14 A
First setup the string to use in your IN statement. Here you are putting the string into "str_in_statement". We are using COLUMN NEW_VALUE and LISTAGG to setup the string.
clear columns
COLUMN temp_in_statement new_value str_in_statement
SELECT DISTINCT
LISTAGG('''' || myLetter || ''' AS ' || myLetter,',')
WITHIN GROUP (ORDER BY myLetter) AS temp_in_statement
FROM (SELECT DISTINCT myLetter FROM myTable);
Your string will look like:
'A' AS A,'B' AS B,'C' AS C
Now use the String statement in your PIVOT query.
SELECT * FROM
(SELECT myNumber, myLetter, myValue FROM myTable)
PIVOT (Sum(myValue) AS val FOR myLetter IN (&str_in_statement));
Here is the Output:
MYNUMBER A_VAL B_VAL C_VAL
---------- ---------- ---------- ----------
1 2 4
2 8 10 6
3 14 12
There are limitations though. You can only concatenate a string up to 4000 bytes.
You can't put a non constant string in the IN clause of the pivot clause.
You can use Pivot XML for that.
From documentation:
subquery A subquery is used only in conjunction with the XML keyword.
When you specify a subquery, all values found by the subquery are used
for pivoting
It should look like this:
select xmlserialize(content t.B_XML) from t_aa
pivot xml(
sum(A) for B in(any)
) t;
You can also have a subquery instead of the ANY keyword:
select xmlserialize(content t.B_XML) from t_aa
pivot xml(
sum(A) for B in (select cl from t_bb)
) t;
Here is a sqlfiddle demo
For later readers, here is another solution
https://technology.amis.nl/2006/05/24/dynamic-sql-pivoting-stealing-antons-thunder/
allowing a query like
select * from table( pivot( 'select deptno, job, count(*) c from scott.emp group by deptno,job' ) )
I am not exactly going to give answer for the question OP has asked, instead I will be just describing how dynamic pivot can be done.
Here we have to use dynamic sql, by initially retrieving the column values into a variable and passing the variable inside dynamic sql.
EXAMPLE
Consider we have a table like below.
If we need to show the values in the column YR as column names and the values in those columns from QTY, then we can use the below code.
declare
sqlqry clob;
cols clob;
begin
select listagg('''' || YR || ''' as "' || YR || '"', ',') within group (order by YR)
into cols
from (select distinct YR from EMPLOYEE);
sqlqry :=
'
select * from
(
select *
from EMPLOYEE
)
pivot
(
MIN(QTY) for YR in (' || cols || ')
)';
execute immediate sqlqry;
end;
/
RESULT
If required, you can also create a temp table and do a select query in that temp table to see the results. Its simple, just add the CREATE TABLE TABLENAME AS in the above code.
sqlqry :=
'
CREATE TABLE TABLENAME AS
select * from
USE DYNAMIC QUERY
Test code is below
-- DDL for Table TMP_TEST
--------------------------------------------------------
CREATE TABLE "TMP_TEST"
( "NAME" VARCHAR2(20),
"APP" VARCHAR2(20)
);
/
SET DEFINE OFF;
Insert into TMP_TEST (NAME,APP) values ('suhaib','2');
Insert into TMP_TEST (NAME,APP) values ('suhaib','1');
Insert into TMP_TEST (NAME,APP) values ('shahzad','3');
Insert into TMP_TEST (NAME,APP) values ('shahzad','2');
Insert into TMP_TEST (NAME,APP) values ('shahzad','5');
Insert into TMP_TEST (NAME,APP) values ('tariq','1');
Insert into TMP_TEST (NAME,APP) values ('tariq','2');
Insert into TMP_TEST (NAME,APP) values ('tariq','6');
Insert into TMP_TEST (NAME,APP) values ('tariq','4');
/
CREATE TABLE "TMP_TESTAPP"
( "APP" VARCHAR2(20)
);
SET DEFINE OFF;
Insert into TMP_TESTAPP (APP) values ('1');
Insert into TMP_TESTAPP (APP) values ('2');
Insert into TMP_TESTAPP (APP) values ('3');
Insert into TMP_TESTAPP (APP) values ('4');
Insert into TMP_TESTAPP (APP) values ('5');
Insert into TMP_TESTAPP (APP) values ('6');
/
create or replace PROCEDURE temp_test(
pcursor out sys_refcursor,
PRESULT OUT VARCHAR2
)
AS
V_VALUES VARCHAR2(4000);
V_QUERY VARCHAR2(4000);
BEGIN
PRESULT := 'Nothing';
-- concating activities name using comma, replace "'" with "''" because we will use it in dynamic query so "'" can effect query.
SELECT DISTINCT
LISTAGG('''' || REPLACE(APP,'''','''''') || '''',',')
WITHIN GROUP (ORDER BY APP) AS temp_in_statement
INTO V_VALUES
FROM (SELECT DISTINCT APP
FROM TMP_TESTAPP);
-- designing dynamic query
V_QUERY := 'select *
from ( select NAME,APP
from TMP_TEST )
pivot (count(*) for APP in
(' ||V_VALUES|| '))
order by NAME' ;
OPEN PCURSOR
FOR V_QUERY;
PRESULT := 'Success';
Exception
WHEN OTHERS THEN
PRESULT := SQLcode || ' - ' || SQLERRM;
END temp_test;
I used the above method (Anton PL/SQL custom function pivot()) and it done the job! As I am not a professional Oracle developer, these are simple steps I've done:
1) Download the zip package to find pivotFun.sql in there.
2) Run once the pivotFun.sql to create a new function
3) Use the function in normal SQL.
Just be careful with dynamic columns names. In my environment I found that column name is limited with 30 characters and cannot contain a single quote in it. So, my query is now something like this:
SELECT
*
FROM
table(
pivot('
SELECT DISTINCT
P.proj_id,
REPLACE(substr(T.UDF_TYPE_LABEL, 1, 30), '''''''','','') as Attribute,
CASE
WHEN V.udf_text is null and V.udf_date is null and V.udf_number is NOT null THEN to_char(V.udf_number)
WHEN V.udf_text is null and V.udf_date is NOT null and V.udf_number is null THEN to_char(V.udf_date)
WHEN V.udf_text is NOT null and V.udf_date is null and V.udf_number is null THEN V.udf_text
ELSE NULL END
AS VALUE
FROM
project P
LEFT JOIN UDFVALUE V ON P.proj_id = V.proj_id
LEFT JOIN UDFTYPE T ON V.UDF_TYPE_ID = T.UDF_TYPE_ID
WHERE
P.delete_session_id IS NULL AND
T.TABLE_NAME = ''PROJECT''
')
)
Works well with up to 1m records.
Looks like it became possible without extra development effort since Oracle 19c with introduction of SQL_MACRO (and possibly Polymorphic Table Functions, which I haven't use yet).
create table t as
select
trunc(level/5) as id
, chr(65+mod(level, 5)) as code
, level as val
from dual
connect by level < 10
create function f_pivot
return varchar2 SQL_MACRO(TABLE)
is
l_codes varchar2(1000);
begin
select listagg(
distinct '''' || code
|| ''' as ' || code, ',')
into l_codes
from t;
return
'select *
from t
pivot (
max(val) for code in (
' || l_codes || '))';
end;
/
select *
from f_pivot()
ID | B | C | D | E | A
-: | -: | -: | -: | -: | ---:
0 | 1 | 2 | 3 | 4 | null
1 | 6 | 7 | 8 | 9 | 5
The only issue (in case of SQL_MACRO approach) is that result set doen't change its structure during one session:
insert into t
values(1, 'Q', 100);
commit;
select *
from f_pivot()
ID | B | C | D | E | A
-: | -: | -: | -: | -: | ---:
0 | 1 | 2 | 3 | 4 | null
1 | 6 | 7 | 8 | 9 | 5
But in separate session it works fine:
select dbms_xmlgen.getxml('select * from f_pivot()') as v
from dual
V
<?xml version="1.0"?><ROWSET> <ROW> <ID>0</ID> <B>1</B> <C>2</C> <D>3</D> <E>4</E> </ROW> <ROW> <ID>1</ID> <B>6</B> <C>7</C> <D>8</D> <E>9</E> <A>5</A> <Q>100</Q> </ROW></ROWSET>
Using with function feature dynamic pivot may be used in-place without predefined function:
with function f_pivot1
return varchar2 SQL_MACRO(TABLE)
is
l_codes varchar2(1000);
begin
select listagg(distinct '''' || code || ''' as ' || code, ',')
into l_codes
from t;
return
'select *
from t
pivot (
max(val) for code in (
' || l_codes || '))';
end;
select *
from f_pivot1()
ID | B | C | D | E | A | Q
-: | -: | -: | -: | -: | ---: | ---:
0 | 1 | 2 | 3 | 4 | null | null
1 | 6 | 7 | 8 | 9 | 5 | 100
db<>fiddle here
You cannot put a dynamic statement in the PIVOT's IN statement without using PIVOT XML, but you can use small Technic to use dynamic statement in PIVOT. In PL/SQL, within a string value, two apostrophe is equal to one apostrophes.
declare
sqlqry clob;
search_ids varchar(256) := '''2016'',''2017'',''2018'',''2019''';
begin
search_ids := concat( search_ids,'''2020''' ); -- you can append new search id dynamically as you wanted
sqlqry :=
'
select * from
(
select *
from EMPLOYEE
)
pivot
(
MIN(QTY) for YR in (' || search_ids || ')
)';
execute immediate sqlqry;
end;
There’s no straightforward method for dynamic pivoting in Oracle’s SQL, unless it returns XML type results.
For the non-XML results PL/SQL might be used through creating functions of SYS_REFCURSOR return type
With Conditional Aggregation
CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_cols VARCHAR2(32767);
BEGIN
SELECT LISTAGG( 'SUM( CASE WHEN job_title = '''||job_title||''' THEN 1 ELSE 0 END ) AS "'||job_title||'"' , ',' )
WITHIN GROUP ( ORDER BY job_title )
INTO v_cols
FROM ( SELECT DISTINCT job_title
FROM jobs j );
v_sql :=
'SELECT "HIRE YEAR",'|| v_cols ||
' FROM
(
SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
FROM employees e
JOIN jobs j
ON j.job_id = e.job_id
)
GROUP BY "HIRE YEAR"
ORDER BY "HIRE YEAR"';
OPEN v_recordset FOR v_sql;
DBMS_OUTPUT.PUT_LINE(v_sql);
RETURN v_recordset;
END;
/
With PIVOT Clause
CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_cols VARCHAR2(32767);
BEGIN
SELECT LISTAGG( ''''||job_title||''' AS "'||job_title||'"' , ',' )
WITHIN GROUP ( ORDER BY job_title )
INTO v_cols
FROM ( SELECT DISTINCT job_title
FROM jobs j );
v_sql :=
'SELECT *
FROM
(
SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
FROM employees e
JOIN jobs j
ON j.job_id = e.job_id
)
PIVOT
(
COUNT(*) FOR job_title IN ( '|| v_cols ||' )
)
ORDER BY "HIRE YEAR"';
OPEN v_recordset FOR v_sql;
DBMS_OUTPUT.PUT_LINE(v_sql);
RETURN v_recordset;
END;
/
But there's a drawback with LISTAGG() that's coded ORA-01489: result of string concatenation is too long raises whenever the concatenated string within the first argument exceeds the length of 4000 characters. In this case, the query returning the value of v_cols variable might be replaced with the XMLELEMENT() function nested within XMLAGG() such as
CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_cols VARCHAR2(32767);
BEGIN
SELECT RTRIM(DBMS_XMLGEN.CONVERT(
XMLAGG(
XMLELEMENT(e, 'SUM( CASE WHEN job_title = '''||job_title||
''' THEN 1 ELSE 0 END ) AS "'||job_title||'",')
).EXTRACT('//text()').GETCLOBVAL() ,1),',') AS "v_cols"
FROM ( SELECT DISTINCT job_title
FROM jobs j);
v_sql :=
'SELECT "HIRE YEAR",'|| v_cols ||
' FROM
(
SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
FROM employees e
JOIN jobs j
ON j.job_id = e.job_id
)
GROUP BY "HIRE YEAR"
ORDER BY "HIRE YEAR"';
DBMS_OUTPUT.put_line(LENGTH(v_sql));
OPEN v_recordset FOR v_sql;
RETURN v_recordset;
END;
/
unless the upper limit 32767 for VARCHAR2 type is exceeded. This last method might also be applied for the database with version prior to Oracle 11g Release 2 as they don't contain LISTAGG() function.
Btw, yet LISTAGG() function can be used during the checkout of the v_cols even for very long concatenated string generated without getting ORA-01489 error while the trailing part of the string is truncated through use of ON OVERFLOW TRUNCATE clause if the version for the database is 12.2+ such as
LISTAGG( <concatenated string>,',' ON OVERFLOW TRUNCATE 'THE REST IS TRUNCATED' WITHOUT COUNT )
The function can be invoked as
VAR rc REFCURSOR
EXEC :rc := Get_Jobs_ByYear;
PRINT rc
from SQL Developer's command line
or
BEGIN
:result := Get_Jobs_ByYear;
END;
from Test window of PL/SQL Developer in order to get the result
set.
Demo for generated queries
You can dynamically pivot data in a single SQL statement with the open source program Method4.Pivot.
After installing the package, call the function and pass in a SQL statement as a string. The last column of your SQL statement defines the values, and the second-to-last column defines the column names. The default aggregation function is MAX, which works well for common entity-attribute-value queries like this one:
select * from table(method4.pivot(
q'[
select 'A' name, 1 value from dual union all
select 'B' name, 2 value from dual union all
select 'C' name, 3 value from dual
]'
));
A B C
- - -
1 2 3
The program also supports different aggregation functions through the parameter P_AGGREGATE_FUNCTION, and allows for a custom column name order if you add a column named PIVOT_COLUMN_ID.
The package uses an Oracle Data Cartridge approach similar to Anton's pivot, but Method4.Pivot has several important advantages:
Regular open source program with a repo, installation instructions, license, unit tests, documentation, and comments - not just a Zip file on a blog.
Handles unusual column names.
Handles unusual data types, like floats.
Handles up to 1000 columns.
Provides meaningful error messages for common mistakes.
Handles NULL column names.
Handles 128-character column names.
Prevents misleading implicit conversion.
Hard-parses statements each time to catch underlying table changes.
But most users are still better off creating a dynamic pivot at the application layer or with the pivot XML option.

Stored procedure calling 2 values in parameter declaration

I have a stored procedure written like this:
CREATE OR REPLACE PROCEDURE Proc_location_example(in_data_ID IN VARCHAR2,
in_Location_name IN VARCHAR2)
IS
v_Location_ID NUMBER;
v_data_id NUMBER;
BEGIN
---------------------------------------------------------------------------
lv_prog_name := 'PRoc_location_example';
ln_step := 1;
SELECT Location_ID
INTO v_Location_ID
FROM random.client
WHERE Location_name = in_Location_name;
.....
END;
This procedure is getting 'NY ' passed in as an example for in_location_name. I want to pass in 'NY' and 'nj' to the location_name.
In other words the location name supports 2 name so which will be easiest way to do it?
You can add an expression with an IN operator such as
.. WHERE Location_name = in_Location_name AND in_Location_name IN ('NY','nj')
if case-sensitivity doesn't matter, then use
.. WHERE Location_name = in_Location_name AND REGEXP_LIKE(in_Location_name, 'Ny|nJ','i')
in order to restrict the parameters to those two values.
There are two options that I can think of that would satisfy your question.
Option #1: More Parameters
By adding more parameters you can use those parameters in your query for location IDs. If you are going to potentially more than 2 locations and don't want to keep having to add parameters, Option #2 is probably a better choice.
CREATE OR REPLACE PROCEDURE Proc_location_example (in_location_name1 IN VARCHAR2 DEFAULT NULL,
in_location_name2 IN VARCHAR2 DEFAULT NULL)
IS
TYPE location_id_t IS TABLE OF NUMBER;
v_location_ids location_id_t;
BEGIN
SELECT location_id
BULK COLLECT INTO v_location_ids
FROM (SELECT 101 AS location_id, 'NJ' AS location_name FROM DUAL
UNION ALL
SELECT 102, 'NY' FROM DUAL
UNION ALL
SELECT 103, 'MA' FROM DUAL)
WHERE location_name IN (in_location_name1, in_location_name2);
FOR i IN 1 .. v_location_ids.COUNT
LOOP
DBMS_OUTPUT.put_line ('ID #' || i || ': ' || v_location_ids (i));
END LOOP;
END;
/
BEGIN
Proc_location_example (in_location_name1 => 'NJ', in_location_name2 => 'NY');
END;
/
Option #2: Table Type Parameter
By using a table as your parameter, you can pass an unlimited number of "location names". This does require you to use a pre-defined table type, but this solution is more flexible in the number of "parameters"
CREATE OR REPLACE TYPE location_name_t IS TABLE OF VARCHAR2 (2);
CREATE OR REPLACE PROCEDURE Proc_location_example (in_locations location_name_t)
IS
TYPE location_id_t IS TABLE OF NUMBER;
v_location_ids location_id_t;
BEGIN
SELECT location_id
BULK COLLECT INTO v_location_ids
FROM (SELECT 101 AS location_id, 'NJ' AS location_name FROM DUAL
UNION ALL
SELECT 102, 'NY' FROM DUAL
UNION ALL
SELECT 103, 'MA' FROM DUAL)
WHERE location_name IN (select * from table(in_locations));
FOR i IN 1 .. v_location_ids.COUNT
LOOP
DBMS_OUTPUT.put_line ('ID #' || i || ': ' || v_location_ids (i));
END LOOP;
END;
/
BEGIN
Proc_location_example (in_locations => location_name_t('NJ','NY'));
END;
/

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

ORA-00928: missing SELECT keyword

In MYTABLE there are courses and their predecessor courses.
What I am trying to is to find the courses to be taken after the specified course. I am getting missing SELECT keyword error. Why I am getting this error although I have SELECT statement in FOR statement ? Where am I doing wrong ?
DECLARE
coursename varchar2(200) := 'COURSE_101';
str varchar2(200);
BEGIN
WITH DATA AS
(select (select course_name
from MYTABLE
WHERE predecessors like ('''%' || coursename||'%''')
) str
from dual
)
FOR cursor1 IN (SELECT str FROM DATA)
LOOP
DBMS_OUTPUT.PUT_LINE(cursor1);
END LOOP;
end;
Unless I'm wrong, WITH factoring clause can't be used that way; you'll have to use it as an inline view, such as this:
declare
coursename varchar2(200) := 'COURSE_101';
str varchar2(200);
begin
for cursor1 in (select str
from (select (select course_name
from mytable
where predecessors like '''%' || coursename||'%'''
) str
from dual
)
)
loop
dbms_output.put_line(cursor1.str);
end loop;
end;
/
Apart from the fact that it doesn't work (wrong LIKE condition), you OVERcomplicated it. This is how it, actually, does something:
SQL> create table mytable(course_name varchar2(20),
2 predecessors varchar2(20));
Table created.
SQL> insert into mytable values ('COURSE_101', 'COURSE_101');
1 row created.
SQL>
SQL> declare
2 coursename varchar2(20) := 'COURSE_101';
3 begin
4 for cursor1 in (select course_name str
5 from mytable
6 where predecessors like '%' || coursename || '%'
7 )
8 loop
9 dbms_output.put_line(cursor1.str);
10 end loop;
11 end;
12 /
COURSE_101
PL/SQL procedure successfully completed.
SQL>
Also, is that WHERE clause correct? PREDECESSORS LIKE COURSENAME? I'm not saying that it is wrong, just looks somewhat strange.
To extend #Littlefoot's answer a bit: you can use a common table expression (WITH clause) in your cursor, but the WITH must be part of the cursor SELECT statement, not separate from it:
DECLARE
coursename varchar2(200) := 'COURSE_101';
BEGIN
FOR aRow IN (WITH DATA AS (select course_name AS str
from MYTABLE
WHERE predecessors like '''%' || coursename||'%''')
SELECT str FROM DATA)
LOOP
DBMS_OUTPUT.PUT_LINE(aRow.str);
END LOOP;
END;
Also note that the iteration variable in a cursor FOR-loop represents a row returned by the cursor's SELECT statement, so if you want to display whatever was returned by the cursor you must use dotted-variable notation (e.g. aRow.str) to extract fields from the row.
Best of luck.
CREATE TABLE product
(
PRODUCT_ID int Primary key,
NAME VARCHAR (20) not null,
Batchno int not null,
Rate int not null,
Tax int not null,
Expiredate date not null
);
INSERT INTO PRODUCT VALUSES(1 , 'vasocare', 32 , 15 , 2 , 01-JAN-2021);

How to use variables in Oracle PL/SQL Function

I'm sorry upfront because this question seems to easy.
I have this function:
CREATE OR REPLACE FUNCTION Costs_MK (VIEWNAME IN VARCHAR2 , WHERE_CLAUSE IN VARCHAR2)
RETURN VARCHAR2
IS
v_Costs VARCHAR2 (500);
BEGIN
Select Listagg(Costs, ';' ) WITHIN GROUP (ORDER BY Costs)
into v_Costs
from (select distinct (Costs)
from VIEWNAME
where WHERE_CLAUSE);
RETURN v_Costs;
END Costs_MK;
However I get the Error-Message:
Error(13,30): PL/SQL: ORA-00920: invalid relational operator
I even can't compile it. If I use the exact values for Viewname and Where_clause I get the desired result.
What am I doing wrong?
/edit: Line 13 is
from VIEWNAME
/edit #2:
Thanks guys. You helped me a lot. I didn't thought about dynamic sql in the first step, so thanks for the refresher ;).
I suggest you to add EXCEPTION BLOCK along with EXECUTE IMMEDIATE
I have created a PROCEDURE you can similary create FUNCTION
CREATE OR REPLACE procedure Costs_PK(VIEWNAME IN VARCHAR2 , WHERE_CLAUSE IN VARCHAR2 )
AS
v_Costs VARCHAR2 (500);
sql_stmnt varchar2(2000);
BEGIN
sql_stmnt := 'Select Listagg(Cost, '';'' ) WITHIN GROUP (ORDER BY Cost) from (select distinct (Cost) from ' || VIEWNAME || ' where ' || WHERE_CLAUSE || ' ) ';
--sql_stmnt := 'Select Listagg(Cost, '';'' ) WITHIN GROUP (ORDER BY Cost) from (select distinct (Cost) from cost_tab where cost >=123 ) ';
EXECUTE IMMEDIATE sql_stmnt INTO v_Costs ;
dbms_output.put_line ('query works -- ' || v_costs);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE ('input :' || VIEWNAME || ' and ' || WHERE_CLAUSE );
dbms_output.put_line (sql_stmnt );
DBMS_OUTPUT.PUT_LINE ('ERROR MESSAGE : ' || sqlCODE || ' ' || SQLERRM );
END;
begin
Costs_PK('cost_tab','cost >= 123');
end;
NOTE: code has been Tested
output:
query works -- 123;456
This is one of the areas in PL/SQL where the most straightforward static SQL solution requires code duplication as there is no way to parametrize the table name in a query. Personally I usually favor duplicate code of static SQL over the increased complexity of dynamic SQL as I like PL/SQL compiler to check my SQL compile time. YMMV.
You don't tell us what kind of where statements the different views are having. In the example below I assume there is 1:1 relation between the view and where parameter(s) so I can easily build static SQL.
create or replace view foo_v (foo_id, cost) as
select level, level*10 from dual connect by level < 10
;
create or replace view bar_v (bar_id, cost) as
select level, level*100 from dual connect by level < 10
;
create or replace function cost_mk(
p_view in varchar2
,p_foo_id in number default null
,p_bar_id in number default null
) return varchar2 is
v_cost varchar2(32767);
begin
case lower(p_view)
when 'foo_v' then
select listagg(cost, ';' ) within group (order by cost)
into v_cost
from (select distinct cost
from foo_v
where foo_id < p_foo_id);
when 'bar_v' then
select listagg(cost, ';' ) within group (order by cost)
into v_cost
from (select distinct cost
from bar_v
where bar_id < p_bar_id);
end case;
return v_cost;
end;
/
show errors
Usage example
select cost_mk(p_view => 'foo_v', p_foo_id => 5) from dual;
select cost_mk(p_view => 'bar_v', p_bar_id => 5) from dual;
You would want to use EXECUTE IMMEDIATE as was hinted by the comments to your question, define a new variable sqlQuery VARCHAR2(200); or similar and rework your sql similar to the following:
sqlQuery := 'Select Listagg(Costs, '';'' ) WITHIN GROUP (ORDER BY Costs) ';
sqlQuery := sqlQuery || 'from (select distinct (Costs) from :1 where :2)';
EXECUTE IMMEDIATE sqlQuery INTO v_Costs USING VIEWNAME, WHERE_CLAUSE;

Resources