How to do conditional processing in a bulk collect loop? - oracle

we have Oracle 11G and i'm trying to move data from one table to another using bulk collect. Problem is when I tried to evaluate if one field from origin is empty my package got invalidated. What I have:
Declaration:
CREATE OR REPLACE PACKAGE MYSCHEMA.MYPKG AS
CURSOR CUR_MYDATA IS
SELECT
o.name,
o.last_name,
o.id,
o.socnum
FROM
origin o
WHERE
1=1
AND o.name like upper ('a%');
TYPE t_name IS TABLE OF origin.name%TYPE;
TYPE t_lastname IS TABLE OF origin.last_name%TYPE;
TYPE t_id IS TABLE OF origin.id%TYPE;
TYPE t_socnum IS TABLE OF origin.socnum%TYPE;
l_name t_name;
l_lastname t_lastname;
l_id t_id;
l_socnum t_socnum;
PROCEDURE MYPROCEDURE;
END MYPKG;
Body:
CREATE OR REPLACE PACKAGE BODY MYSCHEMA.MYPKG AS
PROCEDURE MYPROCEDURE IS
BEGIN
OPEN CUR_MYDATA;
LOOP
FETCH CUR_MYDATA BULK COLLECT INTO l_name,l_lastname,l_id,l_socnum;
forall i IN 1 .. l_name.COUNT
IF ( l_socnum(i) IS NULL)
THEN (select oo.socnum from other_origin where oo.id=l_id(i))
END IF;
INSERT INTO destiny (
d_name,
d_lastname,
d_id,
d_socnum)
VALUES (
l_name(i),
l_lastname(i),
l_id(i),
l_socnum(i),
EXIT WHEN l_name.count = 0;
END LOOP;
END MYPROCEDURE;
END MYPKG;
but when I check body status it is INVALID
any thoughs?

FORALL is not a loop construct: it cannot be split from its DML statement.
when I tried to evaluate if one field from origin is empty
You need to loop round the populated collection and fix that before executing the FORALL ... INSERT.
CREATE OR REPLACE PACKAGE BODY MYSCHEMA.MYPKG AS
PROCEDURE MYPROCEDURE IS
BEGIN
OPEN CUR_MYDATA;
LOOP
FETCH CUR_MYDATA BULK COLLECT INTO l_name,l_lastname,l_id,l_socnum;
EXIT WHEN l_name.count = 0;
for idx in 1 .. l_socnum.count() loop
IF l_socnum(idx) IS NULL THEN
select oo.socnum
into l_socnum(idx)
from other_origin
where oo.id = l_id(idx);
END IF;
end loop;
forall i IN 1 .. l_name.COUNT
INSERT INTO destiny (
d_name,
d_lastname,
d_id,
d_socnum)
VALUES (
l_name(i),
l_lastname(i),
l_id(i),
l_socnum(i));
END LOOP;
END MYPROCEDURE;
END MYPKG;
Other notes.
Check whether the fetch returns any records immediately after executing the fetch. Otherwise your code will attempt to execute code over an empty collection, which will fail.
You should define a collection based on the target table %rowtype: this is simpler than defining and handling multiple collections based on columns.
Also, your real code may be way more complicated than what you posted here, but if you have a large amount of data to shift there is a lot of performance gain in using pure SQL rather than a procedure:
INSERT INTO DESTINY (
D_NAME,
D_LASTNAME,
D_ID,
D_SOCNUM
)
SELECT
o.name,
o.last_name,
o.id,
coalesce(o.socnum, oo.socnum)
FROM
origin o
left outer join other_origin oo
on oo.id = o.id
WHERE
1=1
AND o.name like upper ('a%');

IF condition is not allowed inside FOR ALL.
FOR ALL can execute a single DML: INSERT, UPDATE, or DELETE statement which is written following it. It is not normal for loop.
You can try the following code:
Package:
CREATE OR REPLACE PACKAGE MYSCHEMA.MYPKG AS
CURSOR CUR_MYDATA IS
SELECT
O.NAME,
O.LAST_NAME,
O.ID,
-- ADDED THIS CASE STATEMENT
CASE
WHEN O.SOCNUM IS NOT NULL THEN O.SOCNUM
ELSE OO.SOCNUM
END AS SOCNUM
FROM
-- ADDED THIS LEF JOIN
ORIGIN O
LEFT JOIN OTHER_ORIGIN OO ON ( OO.ID = O.ID )
WHERE
1 = 1
AND O.NAME LIKE UPPER('a%');
TYPE T_NAME IS
TABLE OF ORIGIN.NAME%TYPE;
TYPE T_LASTNAME IS
TABLE OF ORIGIN.LAST_NAME%TYPE;
TYPE T_ID IS
TABLE OF ORIGIN.ID%TYPE;
TYPE T_SOCNUM IS
TABLE OF ORIGIN.SOCNUM%TYPE;
L_NAME T_NAME;
L_LASTNAME T_LASTNAME;
L_ID T_ID;
L_SOCNUM T_SOCNUM;
PROCEDURE MYPROCEDURE;
END MYPKG;
Package Body
CREATE OR REPLACE PACKAGE BODY MYSCHEMA.MYPKG AS
PROCEDURE MYPROCEDURE IS
BEGIN
OPEN CUR_MYDATA;
FETCH CUR_MYDATA BULK COLLECT INTO
L_NAME,
L_LASTNAME,
L_ID,
L_SOCNUM
LIMIT 1000;
FORALL I IN 1..L_NAME.COUNT
--
-- REMOVED THIS CONDITION
--
-- IF ( l_socnum(i) IS NULL)
-- THEN (select oo.socnum from other_origin where oo.id=l_id(i))
-- END IF;
INSERT INTO DESTINY (
D_NAME,
D_LASTNAME,
D_ID,
D_SOCNUM
) VALUES (
L_NAME(I),
L_LASTNAME(I),
L_ID(I),
L_SOCNUM(I)
);
CLOSE CUR_MYDATA;
END MYPROCEDURE;
END MYPKG;

Related

Oracle put resultset into variable in FORALL

I have the following plsql block
declare
TYPE t_mds_ids IS TABLE OF mds.id%TYPE;
l_mds_ids t_mds_ids;
l_mds_parents t_mds_parents;
begin
SELECT id BULK COLLECT INTO l_mds_ids FROM mds;
FORALL indx IN l_mds_ids.FIRST .. l_mds_ids.LAST
select l_mds_ids(indx), ch.id_employee_parent
into l_mds_parents
FROM hierarchy_all ch
CONNECT BY ch.id_employee = prior ch.id_employee_parent
START WITH ch.id_employee = l_mds_ids(indx);
EXECUTE IMMEDIATE 'truncate table mds_hierarchy_all';
insert into mds_hierarchy_all
select * from l_mds_parents;
end;
t_mds_parents declared as
create or replace type r_mds_parents as object (
id_mds number(5,0),
id_employee number(5,0)
);
/
create or replace type t_mds_parents as table of r_mds_parents;
/
And I get an exception ORA-00947: not enough values
I really need to put the resultset of multiple rows into variable of TABLE TYPE on each iteration of FORALL loop. I can't use BULK COLLECT into l_mds_parents as it's restricted inside of FORALL.
Is there only solution to use temporary table instead of table variable?
I don't think you can do this with forall. You could use nested loops:
declare
TYPE t_mds_ids IS TABLE OF mds.id%TYPE;
l_mds_ids t_mds_ids;
l_mds_parents t_mds_parents;
begin
SELECT id BULK COLLECT INTO l_mds_ids FROM mds;
l_mds_parents := NEW t_mds_parents();
FOR indx IN l_mds_ids.FIRST .. l_mds_ids.LAST LOOP
FOR rec IN (
select l_mds_ids(indx) as id_employee, ch.id_employee_parent
FROM hierarchy_all ch
CONNECT BY ch.id_employee = prior ch.id_employee_parent
START WITH ch.id_employee = l_mds_ids(indx)
) LOOP
l_mds_parents.extend();
l_mds_parents(l_mds_parents.COUNT)
:= NEW r_mds_parents (rec.id_employee, rec.id_employee_parent);
END LOOP;
END LOOP;
EXECUTE IMMEDIATE 'truncate table mds_hierarchy_all';
insert into mds_hierarchy_all
select * from table(l_mds_parents);
end;
/
But you don't need to use PL/SQL at all; use a single hierarchical query, or probably more simply here, recursive subquery factoring:
insert into mds_hierarchy_all /* (id_mds, id_employee) -- better to list columns */
with rcte (id_mds, id_employee) as (
select m.id, ha.id_employee_parent
from mds m
join hierarchy_all ha on ha.id_employee = m.id
union all
select r.id_mds, ha.id_employee_parent
from rcte r
join hierarchy_all ha on ha.id_employee = r.id_employee
)
select * from rcte;
db<>fiddle with some made-up data.

PLS-00497: cannot mix between single row and multi-row (BULK) in INTO list

I have created a procure to display the data in two table using BULK COLLECT, but i keep getting this error.
PLS-00497: cannot mix between single row and multi-row (BULK) in INTO list
However it works if i remove the BULK COLLECT and include a where clause in the statement.
create or replace PROCEDURE sktReport IS
TYPE inventory_table_type is RECORD (
v_WH_ID INVENTORY.WH_ID%TYPE,
v_wa_Product_quantity_id Product_quantity.ST_ID%TYPE);
v_inventory_table inventory_table_type;
BEGIN
SELECT INVENTORY.WH_ID, Product_quantity.ST_ID,
BULK COLLECT INTO
v_inventory_table.v_WH_ID,
v_inventory_table.v_wa_Product_quantity_id,
FROM INVENTORY
INNER JOIN Product_quantity
ON Product_quantity.ST_ID = INVENTORY.ST_ID;
FOR i IN v_inventory_table.v_WH_ID..v_inventory_table.v_WH_ID
LOOP
DBMS_OUTPUT.PUT_LINE ('ID : ' || v_inventory_table.v_WH_ID
|| ' quantity ID : ' || v_inventory_table.v_in_Product_quantity_id);
END LOOP;
END;
Here's a simple showcase, I've just written that might help you :)
SET SERVEROUTPUT ON;
DECLARE
TYPE t_some_type IS RECORD (
the_id NUMBER
,the_name VARCHAR2(1)
);
TYPE t_some_type_tab IS TABLE OF t_some_type;
lt_some_record t_some_type;
lt_some_array t_some_type_tab := NEW t_some_type_tab();
BEGIN
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
BULK COLLECT INTO -- use this to select into an array/collection type variable
lt_some_array
FROM
some_values sv;
DBMS_OUTPUT.PUT_LINE(lt_some_array.COUNT);
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
INTO -- use this to select into a regular variables
lt_some_record.the_id
,lt_some_record.the_name
FROM
some_values sv
WHERE
sv.the_id = 1;
DBMS_OUTPUT.PUT_LINE(lt_some_record.the_id||': '||lt_some_record.the_name);
-- you can also insert such record into your array type variable
lt_some_array := NEW t_some_type_tab();
lt_some_array.EXTEND; -- extend the array type variable (so it could store one more element, than now - which was 0)
lt_some_array(lt_some_array.LAST) := lt_some_record; -- assign the first element of array type variable
DBMS_OUTPUT.PUT_LINE(lt_some_array.COUNT||' '||lt_some_array(lt_some_array.LAST).the_id||': '||lt_some_array(lt_some_array.LAST).the_name);
END;
/
Also, since you want to iterate through your results, you can just use cursor (implicit or explicit) e.g.
DECLARE
-- cursor declaration
CURSOR c_some_cursor IS
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
FROM
some_values sv;
BEGIN
-- using explicit, earlier declared cursor
FOR c_val IN c_some_cursor
LOOP
DBMS_OUTPUT.PUT_LINE(c_val.the_id||': '||c_val.the_name);
END LOOP;
-- using implicit, not declared cursor
FOR c_val IN (
WITH some_values AS (
SELECT
DECODE(LEVEL,1,1,2,2,3,3,4,4) AS the_id
,DECODE(LEVEL,1,'A',2,'B',3,'C',4,'D') AS the_name
FROM
dual
CONNECT BY LEVEL < 5
)
SELECT
sv.the_id
,sv.the_name
FROM
some_values sv
)
LOOP
DBMS_OUTPUT.PUT_LINE(c_val.the_id||': '||c_val.the_name);
END LOOP;
END;
/
Here i have demonstrated a simple example to replicate your scenario. Please see below code. This may help you out.
SET serveroutput ON;
DECLARE
TYPE AV_TEST
IS
RECORD
(
lv_att1 PLS_INTEGER,
lv_att2 PLS_INTEGER );
type av_test_tab
IS
TABLE OF av_test;
av_test_tab_av av_test_tab;
BEGIN
NULL;
SELECT LEVEL,
LEVEL+1 BULK COLLECT
INTO av_test_tab_av
FROM DUAL
CONNECT BY LEVEL < 10;
dbms_output.put_line(av_test_tab_av.count);
FOR I IN av_test_tab_av.FIRST..av_test_tab_av.LAST
LOOP
dbms_output.put_line('working fine '||av_test_tab_av(i).lv_att1||' '||av_test_tab_av(i).lv_att2);
END LOOP;
END;

Storing an entire column in a variable inside a stored procedure

I have a complex stored procedure inside a package. Inside this SP, I need to query a table to get all the data pertaining one column and then use this to check some other condition inside an "IF" statement.
Here is what I am doing:
--declare a variable to store the holidays
l_holidays MySchema. HolidayTable.DateColumn%TYPE
-- populate this variable
Select a.DateColumn into l_holidays
from MySchema. HolidayTable a;
-- using this variable inside an "IF" statement
IF (current_Date IN l_holidays)
THEN
-- do something
ELSE
-- do something
END IF;
Every time I run this, I get the following error
ORA-01422: exact fetch returns more than requested number of rows
I know this is because I am trying to populate the entire column using the "INTO" clause. But I don't know any other way of doing it.
Create a collection and use BULK COLLECT INTO:
CREATE PROCEDURE my_proc (
current_date IN MySchema.HolidayTable.DateColumn%TYPE
)
AS
TYPE date_tab IS TABLE OF MySchema.HolidayTable.DateColumn%TYPE;
l_holidays date_tab;
BEGIN
SELECT DateColumn
BULK COLLECT INTO l_holidays
FROM MySchema.HolidayTable;
IF (current_Date MEMBER OF l_holidays)
THEN
NULL; -- do something
ELSE
NULL; -- do something
END IF;
END;
Otherwise you can just test in the select:
CREATE PROCEDURE my_proc (
current_date IN MySchema.HolidayTable.DateColumn%TYPE
)
AS
has_date NUMBER(1,0);
BEGIN
SELECT CASE WHEN EXISTS ( SELECT 'X'
FROM MySchema.HolidayTable
WHERE DateColumn = Current_Date )
THEN 1
ELSE 0
END
INTO has_date
FROM DUAL;
IF has_date = 1
THEN
NULL; -- do something
ELSE
NULL; -- do something
END IF;
END;
Hello similarly you can use this query to fulfill your requirements
SET serveroutput ON;
SET sqlbl ON;
DECLARE
type l_holiday
IS
TABLE OF DATE;
tab_holiday l_holiday;
BEGIN
SELECT a.dt BULK COLLECT
INTO tab_holiday
FROM
(SELECT SYSDATE dt FROM DUAL
UNION
SELECT SYSDATE+1 dt FROM DUAL
UNION
SELECT SYSDATE+2 dt FROM DUAL
UNION
SELECT SYSDATE+3 FROM DUAL
)a;
IF tab_holiday.COUNT > 0 THEN
IF SYSDATE MEMBER OF tab_holiday THEN
dbms_output.put_line('yes working');
ELSE
dbms_output.put_line('awsme working');
END IF;
END IF;
END;

Creating a Cursor and Procedure

I created a Cursor for a procedure. I am trying to apply a flag to records in that cursor.
Create or Replace Procedure Pledges
(IDdonor In Int)
is
Cursor Cur_Pledges is
Select dd_pledge.iddonor, dd_status.idstatus from dd_donor
join dd_pledge on dd_donor.iddonor=dd_pledge.iddonor
join dd_status on dd_pledge.idstatus=dd_status.idstatus;
Type All_Pledges2 is record(iddonor dd_pledge.iddonor%type, idstatus dd_status.idstatus%type, flag Varchar2(10));
Begin
For Rec_Pledges in Cur_Pledges LOOP
if rec_pledges.idstatus = '10' THEN Flag := 'True';
elsif rec_pledges.idstatus= '20' THEN Flag := 'False';
End if;
Insert Into All_Pledges
Values(rec_pledges.idddonor, rec_pledges.idstatus, flag);
End Loop;
End;
You are wrongly using the type record variable, I have made the changes please check below, this will work:
Create or Replace Procedure Pledges
(IDdonor In Int)
is
Cursor Cur_Pledges is
Select dd_pledge.iddonor, dd_status.idstatus from dd_donor
join dd_pledge on dd_donor.iddonor=dd_pledge.iddonor
join dd_status on dd_pledge.idstatus=dd_status.idstatus;
Type All_Pledges2 is record(iddonor dd_pledge.iddonor%type, idstatus dd_status.idstatus%type, flag Varchar2(10));
-- new change below
allpledges2 All_Pledges2;
Begin
For Rec_Pledges in Cur_Pledges LOOP
if rec_pledges.idstatus = '10' THEN
allpledges2.Flag := 'True';
elsif rec_pledges.idstatus= '20' THEN
allpledges2.Flag := 'False';
End if;
Insert Into All_Pledges
Values(rec_pledges.iddonor, rec_pledges.idstatus, allpledges2.flag);
End Loop;
End;
While you can do this with a cursor loop, you shouldn't. Generally, PL/SQL performs best if you minimize the number of context changes and let the SQL optimizer do it's job. This procedure should consist of a single insert statement:
CREATE OR REPLACE PROCEDURE pledges (iddonor IN INT) IS
BEGIN
INSERT INTO all_pledges
SELECT dd_pledge.iddonor,
dd_status.idstatus,
CASE dd_status.idstatus
WHEN '10' THEN 'True'
WHEN '20' THEN 'False'
END
FROM dd_donor
JOIN dd_pledge ON dd_donor.iddonor = dd_pledge.iddonor
JOIN dd_status ON dd_pledge.idstatus = dd_status.idstatus;
END pledges;
It probably worth noting that, as written, the iddonor parameter is superfluous: since you aren't referencing it in the code, it serves no purpose.
If the goal is to simply return the results to another program, you don't want an insert at all (using a table to return a result set is not a good pattern in Oracle). The best way to handle that is typically to open a ref cursor and return that:
CREATE OR REPLACE PROCEDURE pledges (iddonor IN INT,
all_pledges OUT SYS_REFCURSOR) IS
BEGIN
OPEN all_pledges FOR
SELECT dd_pledge.iddonor,
dd_status.idstatus,
CASE dd_status.idstatus
WHEN '10' THEN 'True'
WHEN '20' THEN 'False'
END
FROM dd_donor
JOIN dd_pledge ON dd_donor.iddonor = dd_pledge.iddonor
JOIN dd_status ON dd_pledge.idstatus = dd_status.idstatus
WHERE dd_donor.iddonor = pledges.iddonor;
END pledges;
Alternately, you could return a user-defined type or write the results to a global temporary table.

Using like with varchar2 that have null value at start

The following is excerpt from my procedure that creates logical backup after checking the backup_schedule value in schema_info table:
CREATE OR REPLACE PROCEDURE BACKUP_EXECUTE
(
[...]
) AS
[...]
schemas varchar2(255):=' ';
cursor schema_name is select upper(schema_name) schema_name from schema_info
where backup_schedule like '%'||to_char(sysdate, 'D')||'%'
and exists (select * from dba_users where username=upper(schema_name));
type sl is table of schema_name%ROWTYPE
index by pls_integer;
schema_list sl;
BEGIN
open schema_name;
fetch schema_name bulk collect into schema_list;
close schema_name;
if schema_list.count != 0 then
for indx in 1..schema_list.count loop
if(schemas not like '%'||schema_list(indx).schema_name||'%') then
if indx>1 then
schemas:=schemas||',';
else
schemas:=null;
end if;
schemas:=schemas||''''||schema_list(indx).schema_name||'''';
end if;
end loop;
[...]
end if;
EXCEPTION
[...]
END;
Besides the fact, that this can be done better, I want to concentrate on one thing. As we know like is not working with null values. Should I leave the code as above and write some start value which later is deleted or is it better to use nvl(schemas,' ') in if statement?

Resources