How to concatenate strings in oracle pl/sql loop? - oracle

I am getting a string of data from application in the variable Pvalue1= a;b;c;e%;f%;h%; which is input from an excel. In my pl/sql code I want to store them in two different variables like this
v1= 'a';'b';'c';
v2= e%f%h%
I am using the below code but this does not seem to be working as expected.
FOR i in 1..FileRowCount LOOP
if instr(v1,'%')=0 then
v1 := SUBSTR(Pvalue1,0,INSTR(Pvalue1, ';',1,1)-1)||v1
elsif instr(v1,'%')<>0 then
v2 := SUBSTR(Pvalue1,0,INSTR(Pvalue1, ';',1,1)-1)||v2
end if;
Pvalue1 := SUBSTR(Pvalue1,INSTR(Pvalue1, ';',1,1)+1);
end loop;

You can consecutively use REGEXP_SUBSTR() (in order to determine each substring delimited by semi-colons), and INSTR() (in order to determine the substrings with or without % character). Then combine the string trough use of LISTAGG() such as
SQL> SET SERVEROUTPUT ON
SQL> DECLARE
Pvalue1 VARCHAR2(100):='a;b;c;e%;f%;h%;';
v1 VARCHAR2(100);
v2 VARCHAR2(100);
BEGIN
WITH t AS
(
SELECT REGEXP_SUBSTR( Pvalue1, '[^;]+', 1, level ) AS piece, level AS lvl
FROM dual
CONNECT BY level <= REGEXP_COUNT( Pvalue1, ';' )
)
SELECT LISTAGG(CASE WHEN INSTR( piece,'%')=0 THEN ''''||piece||'''' END,';') WITHIN GROUP (ORDER BY lvl) AS v1,
LISTAGG(CASE WHEN INSTR( piece,'%')>0 THEN piece END) WITHIN GROUP (ORDER BY lvl) AS v2
INTO v1, v2
FROM t;
DBMS_OUTPUT.PUT_LINE( 'v1 : '||v1 );
DBMS_OUTPUT.PUT_LINE( 'v2 : '||v2 );
END;
/
v1 : 'a';'b';'c'
v2 : e%f%h%

Related

oracle function for: select distinct values count from comma separated string

I need an Oracle (11) function to handle this question.
I need to counting distinct values count from comma separated string.
For example the comma separated string:
'Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text'
The result have to be = 6
Beacuse of
Lorem
Ipsum
is
simply
dummy
text
I want to use like this
select fn_dist_count_values_in_list_arr('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text') from dual;
Can anyone help to write this ("fn_dist_count_values_in_list_arr") oracle function?
You don't need a context switch from SQL to a PL/SQL function and can do it all in SQL:
SELECT ( SELECT COUNT( DISTINCT CAST(column_value AS VARCHAR2(20)) )
FROM XMLTABLE( ('"'||REPLACE(value,',','","')||'"') ) )
AS num_distinct_values
FROM table_name
Which, for the sample data:
CREATE TABLE table_name ( value ) AS
SELECT 'Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text' FROM DUAL;
Outputs:
| NUM_DISTINCT_VALUES |
| ------------------: |
| 6 |
If you want a pure PL/SQL function (so that you do not have multiple context-switches) then:
CREATE FUNCTION fn_dist_count_values_in_list_arr (
list_value IN VARCHAR2
) RETURN NUMBER DETERMINISTIC
IS
TYPE t_words IS TABLE OF NUMBER(1,0) INDEX BY VARCHAR2(200);
v_words t_words;
v_start PLS_INTEGER := 1;
v_end PLS_INTEGER;
BEGIN
IF list_value IS NULL THEN
RETURN 0;
END IF;
LOOP
v_end := INSTR( list_value, ',', v_start );
EXIT WHEN v_end = 0;
v_words(SUBSTR(list_value, v_start, v_end - v_start ) ) := 1;
v_start := v_end + 1;
END LOOP;
v_words(SUBSTR(list_value,v_start)) := 1;
RETURN v_words.COUNT;
END;
/
and then:
SELECT fn_dist_count_values_in_list_arr( value )
FROM table_name
outputs:
| FN_DIST_COUNT_VALUES_IN_LIST_ARR(VALUE) |
| --------------------------------------: |
| 6 |
db<>fiddle here
CREATE OR REPLACE FUNCTION DIST_COUNT_VALUES_IN_STR_ARR
(STR_ARR IN VARCHAR2)
RETURN NUMBER
AS
DIST_COUNT NUMBER(38);
BEGIN
SELECT COUNT(DISTINCT COL1)
INTO DIST_COUNT FROM (
SELECT REGEXP_SUBSTR(STR_ARR,'[^,]+', 1, LEVEL) COL1
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT(STR_ARR, ',') + 1
);
RETURN DIST_COUNT;
END;
This worked for me. The inner query separates the elements into rows by using regex on the comma character. I had to rename your function as i hit the limit of the max length of an object name for my version of Oracle.
And another approach would be to use hierarchical query to split comma separated values to a set of rows (with clause) and run a simple sql query against it
with str_parsed as (SELECT REGEXP_SUBSTR('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text', '[^,]+', 1, LEVEL) val
FROM dual
CONNECT BY REGEXP_SUBSTR('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text', '[^,]+', 1, LEVEL) IS NOT NULL)
select count(distinct val) from str_parsed

FOR LOOP with WHERE clause

This question has been posed before but not specifically for Oracle database.
Can a FOR LOOP be filtered with WHERE clause? For example I would like to do something like:
--LOG ERRORS
FOR err in c_errors WHERE trx_type='CYCLE_COUNT'
LOOP
...do some stuff
END LOOP;
This code gives error:
PLS-00103: Encountered the symbol "WHERE" when expecting one of the following ...
Is there proper syntax for this?
Here is the cursor definition. It grabs cycle count and adjustment transaction types. But in the log errors section mentioned above, I only want to report on cycle count errors. Sure I could use separate cursors, but was trying to accomplish using one.
CURSOR c_errors IS
SELECT DISTINCT CC_ENTRY_INTERFACE_ID INTERFACE_ID
,ERROR_MESSAGE
,creation_date
,LAST_UPDATE_DATE
,'CYCLE_COUNT' TRX_TYPE
FROM mtl_cc_interface_errors
UNION
SELECT DISTINCT TRANSACTION_INTERFACE_ID
,ERROR_EXPLANATION
,CREATION_DATE
,LAST_UPDATE_DATE
,'ADJUSTMENT'
FROM mtl_transactions_interface
WHERE process_flag=3
AND error_code IS NOT NULL
ORDER BY last_update_date DESC;
FOR err in c_errors WHERE trx_type='CYCLE_COUNT'
This is semantically incorrect.
What you can do, as one of the options, is to create a parameterized cursor.
Here is an example:
Case #1: Parameter is null. All rows returned
set serveroutput on;
declare
cursor l_cursor ( param1 varchar2) is
select *
from (
select level as c1
, 'cycle_count' as trx_type
from dual
connect by level < 3
union all
select level as c1
, 'adjustemnt' as trc_type
from dual
connect by level < 3
) q
where param1 is null
or trx_type = param1;
begin
-- param1 = null. No filter applied
for r in l_cursor(null) loop
dbms_output.put_line('C1: ' || to_char(r.c1) || '; ' ||
'TRX_TYPE: ' || r.trx_type);
end loop;
end;
Result:
CNT: 1; TRX_TYPE: cycle_count
CNT: 2; TRX_TYPE: cycle_count
CNT: 1; TRX_TYPE: adjustemnt
CNT: 2; TRX_TYPE: adjustemnt
Case #1: Filtering by TRX_TYPE
set serveroutput on;
declare
cursor l_cursor ( param1 varchar2) is
select *
from (
select level as c1
, 'cycle_count' as trx_type
from dual
connect by level < 3
union all
select level as c1
, 'adjustemnt' as trc_type
from dual
connect by level < 3
) q
where param1 is null
or trx_type = param1;
begin
-- param1 = 'cycle_count'
for r in l_cursor('cycle_count') loop
dbms_output.put_line('C1: ' || to_char(r.c1) || '; ' ||
'TRX_TYPE: ' || r.trx_type);
end loop;
end;
Result:
C1: 1; TRX_TYPE: cycle_count
C1: 2; TRX_TYPE: cycle_count
Nick's answer it's a nice explanation.
Here is another example of how to use it, by declaring the cursor inline with the FOR syntax:
BEGIN
FOR r_product IN (
SELECT
product_name, list_price
FROM
products
WHERE list_price > 120
ORDER BY list_price DESC
)
LOOP
dbms_output.put_line( r_product.product_name ||
': $' ||
r_product.list_price );
END LOOP;
END;
Source:
https://www.oracletutorial.com/plsql-tutorial/plsql-cursor-for-loop/

pl/sql function to parse string to token in oracle

I have written this code
Select replace (upper(substr(div_no,1,3), ',' ','',''')
from dual
I want when user write div_no like this ('v0e200,q0e600') must return
v0e,q0e
If I have a string like 's05200 , s02700' I want to take first three characters, like 's05 ',' s02'
It seems you're looking for the translate() function :
SQL> select translate ('vwe200,qwe600', ',1234567890', ',')
2 from dual;
TRANSLA
-------
vwe,qwe
SQL>
So your revised test data clarifies the requirement. Here is a regex solution for extracting the first three characters from each segment of a string:
SQL> with testdata as (
2 select 'vwe200,qwe600' as str from dual union all
3 select 's05200 , e0300' as str from dual
4 )
5 select regexp_replace(str, '([a-z0-9]{3})([a-z0-9]*)(,?)','\1\3')
6 from testdata;
REGEXP_REPLACE(STR,'([A-Z0-9]{3})([A-Z0-9]*)(,?)','\1\3')
--------------------------------------------------------------------------------
vwe,qwe
s05 , e03
SQL>
If removing the trailing spaces is required it can be done by adjusting the regex pattern :
SQL> with testdata as (
2 select 'vwe200,qwe600' as str from dual union all
3 select 's05200 , e0300' as str from dual
4 )
5 select regexp_replace(str, '( *)([a-z0-9]{3})([a-z0-9 ]*)(,?)','\2\4')
6 from testdata;
REGEXP_REPLACE(STR,'([A-Z0-9]{3})([A-Z0-9]*)(,?)','\1\3')
--------------------------------------------------------------------------------
vwe,qwe
s05,e03
SQL>
Injecting additional quotes is a use for the replace() function:
SQL> select replace('s05 , e03', ',', ''',''') from dual;
REPLACE('S0
-----------
s05 ',' e03
SQL>
Use the output from the previous answer as the input for this one.
Oracle 12c Query:
with FUNCTION getFirst3CharsOfDelimitedList(
str IN VARCHAR2
) RETURN VARCHAR2
IS
val VARCHAR2(4000);
i INTEGER := 1;
occ INTEGER := 1;
BEGIN
val := SUBSTR( str, i, 3 );
LOOP
i := INSTR( str, ',', 1, occ );
EXIT WHEN i = 0;
LOOP
EXIT WHEN SUBSTR( str, i + 1, 1 ) <> ' ';
i := i + 1;
END LOOP;
val := val || ',' || SUBSTR( str, i + 1, 3 );
occ := occ + 1;
END LOOP;
RETURN val;
END getFirst3CharsOfDelimitedList;
testdata as (
select 'vwe200,qwe600' as str from dual union all
select 's05200 , e0300' as str from dual
)
SELECT getFirst3CharsOfDelimitedList( str ) FROM testdata;
Output:
GETFIRST3CHARSOFDELIMITEDLIST(STR)
----------------------------------
vwe,qwe
s05,e03

Count of each characters in given string

I require the count of each character in a string.
Example:
SELECT ('aabcccdee') from dual;
Result:
a(1),b(2), c(3), d(1),e(2).
Thanks in advance.
You can use a hierarchical query to split your string into it individual characters; this is using a CTE just to supply your example value:
with t (value) as (
select 'aabcccdee' from dual
)
select substr(value, level, 1) as a_char
from t
connect by level <= length(value);
Then you can use aggregation to count how may times each appears:
with t (value) as (
select 'aabcccdee' from dual
)
select a_char, count(*) a_count
from (
select substr(value, level, 1) as a_char
from t
connect by level <= length(value)
)
group by a_char
order by a_char;
A_CH A_COUNT
---- ----------
a 2
b 1
c 3
d 1
e 2
And you can use listagg() (if you're on 11g or above) to aggregate those characters and counts into a single string if that's what you really want:
with t (value) as (
select 'aabcccdee' from dual
)
select listagg(a_char || '(' || count(*) || ')', ',') within group (order by a_char)
from (
select substr(value, level, 1) as a_char
from t
connect by level <= length(value)
)
group by a_char;
LISTAGG(A_CHAR||'('||COUNT(*)||')',',')WITHINGROUP(ORDERBYA_CHAR)
-----------------------------------------------------------------
a(2),b(1),c(3),d(1),e(2)
If you particularly want to do this in PL/SQL - because you value is already in a PL/SQL variable perhaps - you can do the same thing with a context switch:
set serveroutput on
declare
l_value varchar2(30) := 'aabcccdee';
l_result varchar2(100);
begin
select listagg(a_char || '(' || count(*) || ')', ',') within group (order by a_char)
into l_result
from (
select substr(l_value, level, 1) as a_char
from dual
connect by level <= length(l_value)
)
group by a_char;
dbms_output.put_line(l_result);
end;
/
a(2),b(1),c(3),d(1),e(2)
PL/SQL procedure successfully completed.

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