I am selecting same set of records twice, once to return IN REF_CURSOR and then selecting same records to build a CSV so that i can update all records in IN clause .... Can i some how change my procedure to SELECT only once instead of selecting same records twice
PROCEDURE LOADBATCH(
inBUCKET_SIZE IN NUMBER,
OUTCURSOR OUT REF_CURSOR )
AS
V_HANDLE VARCHAR2(2000);
V_LOCK_RESULT INTEGER;
IDs VARCHAR2(2000);
BEGIN
BEGIN
V_HANDLE := GET_LOCK_HANDLE('BATCH');
V_LOCK_RESULT := DBMS_LOCK.REQUEST(V_HANDLE, TIMEOUT => 1);
DBMS_OUTPUT.PUT_LINE(V_LOCK_RESULT);
IF V_LOCK_RESULT <> 1 THEN
OPEN OUTCURSOR FOR SELECT BATCH_ID,INSTRUCTION_ID,INSTRUCTION_DUMP,BATCH_MSG_TYPE,BATCH_AMOUNT,BATCH_CURRENCY,RECIEVED_DATETIME,MODIFIED_DATETIME,SETTLEMENT_DATE,BATCH_STATUS,FROM_MMBID,BATCH_DATE,MODIFICATION_DATETIME,PARENTBATCH_ID,INSTR_REASON FROM
( SELECT DISTINCT BI.*,
BM.*,
BM.AMOUNT AS BATCH_AMOUNT,
BM.CURRENCY AS BATCH_CURRENCY,
BI.PARENT_BATCH_ID AS PARENTBATCH_ID,
BI.REASON AS INSTR_REASON
FROM ACT.BATCH_INSTRUCTIONS BI
INNER JOIN ACT.BATCH_MESSAGES BM
ON BI.BATCH_ID =BM.ID
WHERE (BI.STAGE = 'NEW'
OR (BI.STAGE = 'PICKED'
AND (SYSDATE > (BI.LAST_PICKED_AT + interval '65' second))))
AND (BM.STAGE <> 'COMPLETED')
ORDER BY LAST_PICKED_AT ASC
) WHERE ROWNUM <=inBUCKET_SIZE ;
SELECT listagg(INSTRUCTION_ID, ',') WITHIN GROUP (
ORDER BY INSTRUCTION_ID) AS concatenation
INTO IDs
FROM
(SELECT DISTINCT *
FROM ACT.BATCH_INSTRUCTIONS BI
INNER JOIN ACT.BATCH_MESSAGES BM
ON BI.BATCH_ID =BM.ID
WHERE (BI.STAGE = 'NEW'
OR (BI.STAGE = 'PICKED'
AND (SYSDATE > (BI.LAST_PICKED_AT + interval '65' second)))
)
AND (BM.STAGE <> 'COMPLETED')
ORDER BY LAST_PICKED_AT ASC
)
WHERE ROWNUM <=inBUCKET_SIZE ;
DBMS_OUTPUT.PUT_LINE('IDs are:');
DBMS_OUTPUT.PUT_LINE(IDs);
IF( IDs IS NOT NULL) THEN
UPDATE ACT.BATCH_INSTRUCTIONS
SET LAST_PICKED_AT =sysdate,
STAGE = 'PICKED'
WHERE INSTRUCTION_ID IN
(SELECT INSTRUCTION_ID
FROM ACT.BATCH_INSTRUCTIONS
WHERE INSTRUCTION_ID IN
(SELECT regexp_substr(IDs,'[^,]+', 1, level)
FROM dual
CONNECT BY regexp_substr(IDs, '[^,]+', 1, LEVEL) IS NOT NULL
)
);
COMMIT;
END IF;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END;
-- DBMS_OUTPUT.PUT_LINE('releasing lock:');
V_LOCK_RESULT := DBMS_LOCK.RELEASE(V_HANDLE);
END LOADBATCH;
Wont work with one select because "select into" does not accept dynamic SQL nor cursors.
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/
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.
I have a string which is years separated by comma.
For example 2000,2001,2002,2005,2006,2007 and 2010.
I want to group the consecutive numbers.
My output should be 2000-2003,2005-2007 and 2010. Is there any way to do this in Oracle Stored procedure?
Disclaimer - I don't recommend using this solution "as is", but it can give ideas, and it was fun writing it
I assume you have a column with the csv strings in a table.
If you're using oracle 11gR2 then you can use recursive CTEs-
Here is a sqlfiddle demo
with t as
(
select replace(replace(v, ' and ', ','), ' ','') v
from strings
),
rcte(text, token, res) as
(
select v, regexp_substr(v, '^\d*[^,]'), regexp_substr(v, '^\d*[^,]') ||'-'
from t
union all
select regexp_replace(text, '^\d*,', ''),
regexp_substr(text, '^\d*[^,]'),
case when regexp_substr(text, '^\d*[^,]') = token then
res
when regexp_substr(text, '^\d*[^,]') = token+1 then
regexp_replace(res, '-\d*$', '-'||(token+1))
else rtrim(res, '-') || ',' || regexp_substr(text, '^\d*[^,]') || '-'
end
from rcte
where text <> token
)
select rtrim(res, '-') from rcte
where text = regexp_substr(rtrim(res, '-'), '\d*$');
(This can be done without regular expressions as well)
18:42:15 SYSTEM#dwal> l
1 with p as (
2 select replace('2000,2001,2002,2005,2006,2007 and 2010',' and ',',') s, '[0-9]{4}' r from dual
3 ), ex as (
4 select regexp_substr(s,r, 1, level) as y
5 from p
6 connect by level <= regexp_count(s, r)
7 ), grp as (
8 select connect_by_root(y) s, y
9 from ( select e1.y y, e2.y p from ex e1, ex e2 where e1.y - 1 = e2.y(+) )
10 connect by prior y = p
11 start with p is null
12 ), agg as (
13 select listagg(s||decode(max(y), s, null, '-'||max(y)), ',') within group (order by s) str
14 from grp group by s
15 )
16* select regexp_replace(str, ',', ' and ', 1, regexp_count(str, ',')) result from agg
18:42:16 SYSTEM#dwal> /
RESULT
------------------------------
2000-2002,2005-2007 and 2010
Elapsed: 00:00:00.02
use mathematics
select min(seq) as range_from , max(seq) range_to , count(*) as cnt
from sometable
group by ceil(seq/3) * 3
this part ceil(seq/3) * 3 is rounding a number to the nearest multiple of three. if you want a range of 5 just use ceil(seq/5) * 5.
Cheers !
It can be done with SQL by using analytical functions.
*Update 1: * Answer updated with parser functionality, missed in previous version.
*Update 2: * Added final string composition
with p as ( -- Parameter string
select replace('2000,2001,2002,2005,2006,2007 and 2010',' and ',',') s from dual
),
ex as ( -- Parse string to sequence
select
to_number(
substr(
s,
decode( level, 1, 1, instr(s,',',1,level-1)+1 ),
decode( instr(s,',',1,level), 0, length(s)+1, instr(s,',',1,level) )
-
decode( level, 1, 1, instr(s,',',1,level-1)+1 )
)
) as y
from p
connect by instr(s,',',1,level-1) > 0
),
period_set as (
select -- Make final string for each interval start
y,
lag(y) over (order by y) prior_y,
max(y) over (partition by 1) max_y,
y || (case when is_end > 1 then null else '-' ||end_y end) as interval_string
from
( -- For each start find interval end
select
y,
is_start,
is_end,
lead(y) over (order by y) end_y
from
( -- Find if previous/next value differs more then by one.
-- If so, mark as start/end
select
y,
nvl(y - prev_y, 100) is_start,
nvl(next_y - y, 100) is_end
from
( -- Find previous/next value in sequence
select
y,
lag(y) over (order by y) prev_y,
lead(y) over (order by y) next_y
from ex
)
)
where
is_start > 1 or is_end > 1
)
where is_start > 1
)
select
replace(
substr(
sys_connect_by_path(
decode(y,max_y,'m', null) || interval_string,
','
),2
),
',m',
' and '
) result_str
from
period_set
where
connect_by_isleaf = 1
start with
prior_y is null
connect by
prior y = prior_y
SQL Fiddle can be found here.
A great QUESTION!
Please check my logic...
with test as
(
select '2000,2002,2003,2004,2006,2007' str from dual
)
,test1 as (
select
split1,
lead(split1, 1, null) over (order by split1 asc) lead_no,
level1
from
(
select to_number(regexp_substr (str, '[^,]+', 1, rownum)) split1, level as level1
from test b
connect by level <= length (regexp_replace (str, '[^,]+')) + 1
)x
)
--select * from test1
,test2 (split1, lead_no, level1, op, op1) as(
select
split1,
lead_no,
level1,
(case when split1+1=lead_no then to_char(split1) else NULL end),
(case when split1+1=lead_no then NULL else to_char(split1) end)
from test1
where level1=1
union all
select
a.split1,
a.lead_no,
b.level1+1,
(case when a.split1+1=a.lead_no and to_char(b.op) is not null then to_char(b.op)
when a.split1+1=a.lead_no and to_char(b.op) is null then to_char(a.split1)
else null end),
(case when (a.split1+1<>a.lead_no and to_char(b.op)<>to_char(a.split1)) OR
(a.lead_no is null and to_char(b.op) is not null) then to_char(b.op) ||'-'||to_char(a.split1)
when a.lead_no is null then to_char(a.split1)
else null end)
from test1 a inner join test2 b on a.level1 = b.level1+1
)
select op1 from test2
where op1 is not null
Assuming your data will be comma seperated and input you can mentioned any years seperated by comma.
DECLARE
v_str VARCHAR(100) := '&n';
v_instr NUMBER;
v_instr1 NUMBER;
v_g VARCHAR(50);
v_F VARCHAR(50);
v_OUT VARCHAR(50);
v_OUT1 VARCHAR(50);
v_TEMP VARCHAR(50);
v_TMP VARCHAR(50) := ' ';
v_cnt NUMBER :=0;
V_FLAG NUMBER :=1;
BEGIN
FOR i IN 1..Length(v_str)-Length(REPLACE(v_str,',',''))+1 LOOP
IF i = 1 THEN
v_g := SubStr(v_str,1,InStr(v_str,',',1,i)-1);
V_F := V_G;
ELSE
v_instr := InStr(v_str,',',1,i-1);
v_instr1 := InStr(v_str,',',1,i);
IF(v_cnt+1 <= Length(v_str)-Length(REPLACE(v_str,',',''))) then
v_g := SubStr(v_str,v_instr+1,v_instr1-v_instr-1);
IF V_FLAG = 0 THEN V_F := V_G; V_FLAG :=1; END IF;
ELSE
v_g := SubStr(v_str,v_instr+1);
IF V_FLAG = 0 THEN V_F := V_G; V_FLAG :=1; END IF;
END IF;
END IF;
v_cnt := v_cnt+1;
--IF(I>1) THEN
IF(V_TEMP+1 = V_G) THEN
IF(V_OUT IS not NULL) THEN
V_OUT := V_OUT||'-'||V_G;
ELSE
V_OUT := V_F||'-'||V_G;
END IF;
ELSE
V_OUT1 := SubStr(V_OUT,1,5)||SubStr(V_OUT,-4);
V_OUT := NULL;
v_out := v_g;
V_FLAG := 0;
END IF;
--END IF;
V_TEMP := To_Number(V_G);
--Dbms_Output.put_line(v_g);
IF(v_tmp <> v_out1) THEN
SELECT Decode(instr(v_OUT1,'-'),0,subStr(V_OUT1,1,4),v_out1) INTO v_out1 FROM dual;
Dbms_Output.put_line('Year span : '||v_OUT1);
v_tmp := v_out1;
END IF;
END LOOP;
SELECT Decode(Length(v_OUT),4,v_out,subStr(V_OUT,1,5)||SubStr(V_OUT,-4)) INTO v_out1 FROM dual;
Dbms_Output.put_line('Year span : '||v_out1);
END;
You have to use cursor, to loop throught the years, and to do somethink like this :
CREATE OR REPLACE Function concatYears
RETURN varchar;
DECLARE
oldYear number;
concat varchar(300);
newGroup boolean;
cursor cur1 is
SELECT year
FROM dates
ORDER BY year ASC;
BEGIN
oldYear:=0;
newGroup:=true;
concat := '';
FOR row in c1
IF oldYear == 0 THEN
oldYear := row.year;
END IF;
IF newGroup == true THEN
concat := concat || CAST(oldYear AS varchar(4));
newGroup:= false;
ELSE
IF row.year > oldYear+1 THEN
concat:= concat || '-' || CAST(oldYear AS varchar(4)) || ' , ';
newGroup:=true;
END IF;
END IF;
oldYear:=row.year;
END LOOP;
RETURN concat;
END;