Table variable is filled only with one value - oracle

I have a stored procedure which should return several results - but it returns only one row. I think it's the last row in result set.
I am not sure, but I think the problem is in this line of code:
select chi.id bulk collect into v_numbers from dual;
and that this line somehow overrides all previous results (there is several of them for each loop). How to insert into v_numbers without overriding previous results? I know that it's also wrong to insert only one row, but I haven't found solution to insert several rows from chi.
PROCEDURE GET_ATTRIBUTES(
P_AUTH_USE_ID IN NUMBER,
P_CATEGORY_ID IN NUMBER,
P_VERSION_ID IN NUMBER,
P_RESULT OUT TYPES.CURSOR_TYPE
) IS
v_numbers sys.odcinumberlist := null;
BEGIN
FOR item IN
(SELECT ID FROM INV_SRV WHERE SRV_CATEGORY_ID IN
(
SELECT id
FROM inv_srv_category
START WITH parent_category_id = P_CATEGORY_ID
CONNECT BY PRIOR id = parent_category_id
) OR SRV_CATEGORY_ID = P_CATEGORY_ID)
LOOP
for chi in (select s.id
from inv_srv s
start with s.parent_srv_id = item.id
connect by prior s.id = s.parent_srv_id
)
loop
select chi.id bulk collect into v_numbers from dual; --> here I should insert all rows from that loop, but I don't know how
end loop;
END LOOP;
OPEN P_RESULT FOR SELECT t.column_value from table(v_numbers) t; --> only one row is returned
END;

Use BULK COLLECT and FORALL for bulk inserts and better performance. The FORALL statement will allow the DML to be run for each row in the collection without requiring a context switch each time, thus improving the overall performance.
CREATE OR REPLACE PROCEDURE get_attributes (
p_auth_use_id IN NUMBER,
p_category_id IN NUMBER,
p_version_id IN NUMBER,
p_result OUT types.cursor_type
) IS
v_numbers sys.odcinumberlist := NULL;
BEGIN
SELECT s.id
BULK COLLECT --> Bulk collect all values
INTO v_numbers
FROM inv_srv s
start with s.parent_srv_id in (
SELECT ID FROM INV_SRV
WHERE SRV_CATEGORY_ID IN
(
SELECT id
FROM inv_srv_category
START WITH parent_category_id = P_CATEGORY_ID
CONNECT BY PRIOR id = parent_category_id
)
OR SRV_CATEGORY_ID = P_CATEGORY_ID)
connect by prior s.id = s.parent_srv_id;
FORALL i IN 1..v_numbers.COUNT
INSERT INTO your_table VALUES v_numbers ( i ); --> Bulk insert
END;

Every time the loop executes v_numbers will be re populated again and again so, either 1) use v_numbers.extend; v_numbers(v_numbers.last) = "Your Value" or write everything in a single bulk collect.
select s.id
bulk collect into v_numbers
from inv_srv s
start with s.parent_srv_id in (SELECT ID FROM INV_SRV
WHERE SRV_CATEGORY_ID IN
(
SELECT id
FROM inv_srv_category
START WITH parent_category_id = P_CATEGORY_ID
CONNECT BY PRIOR id = parent_category_id
)
OR SRV_CATEGORY_ID = P_CATEGORY_ID)
connect by prior s.id = s.parent_srv_id

This may be considered as a improper use of the PL/SQL loops (often connected with a catastrophic performance) in a situation where a SQL solution exists.
Why don't you simple defines the cursor as follows:
OPEN P_RESULT FOR
select s.id
from inv_srv s
start with s.parent_srv_id in
(SELECT ID FROM INV_SRV WHERE SRV_CATEGORY_ID IN
(SELECT id
FROM inv_srv_category
START WITH parent_category_id = 1
CONNECT BY PRIOR id = parent_category_id
) OR SRV_CATEGORY_ID = 1)
connect by prior s.id = s.parent_srv_id
;
The query is constructed from your outer and inner loop so that it returns the same result.
The transformation may not be trivial in generall case and must be carefully tested, but the performance profit may be high.

Related

Avoid using same view multiple times inside oracle procedures

I am using a view named V_ENT_MSG in my oracle procedure "TEST PROC" as shown below. The same view is used in multiple places inside the procedure and the procedure execution time is very high. The view returns more than 5 Million records every time. How can I improve procedure execution time. [ Note that I cannot change the view V_ENT_MSG].
create or replace PROCEDURE "TESTPROC"
AS
BEGIN
INSERT
INTO OUTBOUND1
(
DELIVERY_EVENT_ID,
MESSAGE_ID
)
SELECT
V2.DELIVERY_EVENT_ID,PS.MESSAGE_ID
FROM V_ENT_MSG V2,
R_SOURCE PS
WHERE V2.primary_source = PS.SOURCE;
COMMIT;
INSERT
INTO OUTBOUND2
(
DELIVERY_EVENT_ID,
MESSAGE_BODY
)
SELECT
V2.DELIVERY_EVENT_ID,PS2.MESSAGE_BODY
FROM V_ENT_MSG V2,
R_SOURCE2 PS2
WHERE V2.primary_source = PS2.SOURCE;
COMMIT;
INSERT
INTO OUTBOUND3
(
DELIVERY_EVENT_ID,
SUBJECT
)
SELECT
V2.DELIVERY_EVENT_ID,PS3.SUBJECT
FROM V_ENT_MSG V2,
R_SOURCE3 PS3
WHERE V2.primary_source = PS3.SOURCE;
COMMIT;
END TESTPROC;
You may be able to rewrite this as a multitable insert. To do this, you need to:
(Outer) join the view to each table
insert all the result of this query
Use the when clause to control which rows go in which tables
Which looks something like:
insert all
when ps.source is not null then
into outbound1 ( delivery_event_id, message_id )
values ( delivery_event_id, message_id )
when ps2.source is not null then
into outbound2 ( delivery_event_id, message_body )
values ( delivery_event_id, message_body )
when ps3.source is not null then
into outbound3 ( delivery_event_id, subject )
values ( delivery_event_id, subject )
select v.delivery_event_id,ps3.subject
from v_ent_msg v
left join r_source ps
on v.primary_source = ps.source
left join r_source2 ps2
on v.primary_source = ps2.source
left join r_source3 ps3
on v.primary_source = ps3.source;
This assumes that each primary_source joins to at most one row in r_sourceX. If it's 1:M and each value of primary_source can join to a different number of rows in each of the other tables it gets trickier.
You'll need to assign a row_number for each source value in each r_sourceX table and only insert those rows where this is one.
Additionally whether you have opted for GTT as suggested by #Littlefoot or not ,you can still use this approach.
How about leveraging INSERT ALL with WITH clause to achieve the same ,
INSERT ALL
/* inserting to OUTBOUND1 based on table_name = R_SOURCE defined during union */
WHEN table_name = 'R_SOURCE' THEN
INTO outbound1(delivery_event_id, message_id)
VALUES(delivery_event_id, message_type)
/* inserting to OUTBOUND2 based on table_name = R_SOURCE2 defined during union */
WHEN table_name = 'R_SOURCE2' THEN
INTO outbound2(delivery_event_id, message_body)
VALUES (delivery_event_id, message_type)
/* inserting to OUTBOUND3 based on table_name = R_SOURC3 defined during union */
WHEN table_name = 'R_SOURCE3' THEN
INTO outbound3(delivery_event_id, subject)
VALUES (delivery_event_id, message_type)
WITH ent_msg
AS
(SELECT v2.delivery_event_id,v2.primary_source
FROM v_ent_msg v2),
src_data
AS
( SELECT v2.delivery_event_id delivery_event_id,ps.message_id message_type,'R_SOURCE' table_name
FROM r_source ps
JOIN ent_msg v2
ON v2.primary_source = ps.source
UNION ALL
SELECT v2.delivery_event_id,ps2.message_body,'R_SOURCE2' table_name
FROM r_source2 ps2
JOIN ent_msg v2
ON v2.primary_source = ps2.source
UNION ALL
SELECT v2.delivery_event_id,ps3.subject,'R_SOURCE3' table_name
FROM r_source3 ps3
JOIN ent_msg v2
ON v2.primary_source = ps3.source
)
SELECT delivery_event_id,message_type,table_name
FROM src_data
I couldn't validate SQL because of tables are missing.
There are other ways too in case of performance issue where we can go for BULK COLLECT with LIMIT. I am not aware of the data set you are dealing with.
Hope this guides you proceeding with your question.

Is there any faster way to perform merge on 120k records weekly in oracle pl/sql?

There are around 120k records in the database, and based on a few functions I calculate scores for all the records, weekly I have to update the table with new records and respective scores.
Below is a procedure that I am using to merge data into the table:
create or replace procedure scorecalc
AS
score1 number;
score2 number;
score3 number;
CURSOR cur IS
SELECT Id_number from tableA;
r_num cur%ROWTYPE;
BEGIN
--OPEN cur;
FOR r_num IN cur
LOOP
select functionA(r_num.id_number),functionb(r_num.id_number),functionc(r_num.id_number) into score1, score2,score3 from dual;
Merge into scores A USING
(Select
r_num.id_number as ID, score1 as scorea, score2 as scoreb, score3 as scorec, TO_DATE(sysdate, 'DD/MM/YYYY') as scoredate
FROM DUAL) B
ON ( A.ID = B.ID and A.scoredate = B.scoredate)
WHEN NOT MATCHED THEN
INSERT (
ID, scorea, scoreb, scorec, scoredate)
VALUES (
B.ID, B.scorea, B.scoreb, B.scorec,B.scoredate)
WHEN MATCHED THEN
UPDATE SET
A.scorea = B.scorea,
A.scoreb = B.scoreb,
A.scorec = B.scorec;
COMMIT;
END LOOP;
END;
whereas functionA/ B/ C has complex queries, joins in it to calculate the score.
Please suggest me any way to improve the performance because currently with this snippet of code I am only able to insert some 2k records in 1 hour? Can I use parallel DML here?
Thank you!
Why are you doing this in a procedure? This could all be done via DML:
MERGE INTO scores a USING
(SELECT ta.id_number AS ID,
functionA(ta.id) AS scoreA,
functionB(ta.id) AS scoreB,
functionC(ta.id) AS scoreC,
TO_DATE(sysdate, 'DD/MM/YYYY') as scoredate
FROM tableA ta) b
ON (a.id = b.id AND a.scoredate = b.scoredate)
WHEN MATCHED THEN UPDATE SET
a.scorea = b.scorea,
a.scoreb = b.scoreb,
a.scorec = b.scorec
WHEN NOT MATCHED THEN INSERT (ID, scorea, scoreb, scorec, scoredate)
VALUES (B.ID, B.scorea, B.scoreb, B.scorec,B.scoredate);
If you want to try using PARALLEL hint after that, feel free. But you should definitely get rid of that cursor and stop doing "Slow-by-slow" processing.
Something I've had some success with has been inserting from a select statement. It is pretty performant as it doesn't involve row by row inserting.
In your case, I'm thinking it would be something like:
INSERT INTO table (ID, scorea, scoreb, scorec, scoredate)
SELECT functionA(id_number), functionB(id_number), functionC(id_number)
FROM tableA
An example of this can be found at the link below:
https://docs.oracle.com/cd/B12037_01/appdev.101/b10807/13_elems025.htm
To schedule it just put the statement #Del in a procedure block;
create or replace procedure Saturday_Night_Merge is
begin
<Put the merge statement here>
end Saturday_Night_Merge;

How to insert multiple cursor result set into one table

1 table having table structure-
create table tab_abc
( id varchar2(10),
inv_bfr varchar2(20),
desc varchar2(10),
inv_afr varchar2(10) );
I defined 2 cursor here as C1 & C2 ->
cursor C1 is select id, count(inv) AS "inv_bfr", desc from tab_a group by id, desc;
cursor C2 is select count(inv) AS "inv_afr" from tab_a;
Result set of both cursor C1 & C2 will insert into table tab_abc. Cursor C1 will open before one DML operation perform & cursor C2 will open after DML operation perform. Could you please help me can i use OPEN CURSOR THEN FETCH process would be good or FOR CURSOR LOOP INSERT INTO TABLE process.
You don't need to use cursors (or collections, more realistically), or even any PL/SQL here. You can insert data into the table before your 'DML operaton perform' step, and then update if afterwards, e.g. with a merge:
-- initial population
insert into tab_abc (id, inv_bfr, descr, inv_afr)
select id, count(*) as inv_bfr, descr, 0 as inv_after
from tab_a
group by id, descr;
-- intermediate DML operation
-- post-DML update
merge into tab_abc t
using (
select id, 0 as inv_bfr, descr, count(*) as inv_afr
from tab_a
group by id, descr
) afr
on (afr.id = t.id and afr.descr = t.descr)
when matched then
update set inv_afr = afr.inv_afr
when not matched then
insert (id, inv_bfr, descr, inv_afr)
values (afr.id, afr.inv_bfr, afr.descr, afr.inv_afr);
You can wrap all of that in a PL/SQL block if you need to for other reasons, of course.
db<>fiddle demo with a few made-up rows.

Insert into select statement on the same table

I'm currently migrating data from legacy system to the current system.
I have this INSERT statement inside a stored procedure.
INSERT INTO TABLE_1
(PRIMARY_ID, SEQUENCE_ID, DESCR)
SELECT LEGACY_ID PRIMARY_ID
, (SELECT COUNT(*) + 1
FROM TABLE_1 T1
WHERE T1.PRIMARY_ID = L1.LEGACY_ID) SEQUENCE_ID
, L1.DESCR
FROM LEGACY_TABLE L1;
However, whenever I have multiple values of LEGACY_ID from LEGACY_TABLE, the query for the SEQUENCE_ID doesn't increment.
Why is this so? I can't seem to find any documentation on how the INSERT INTO SELECT statement works behind the scenes. I am guessing that it selects all the values from the table you are selecting and then inserts them simultaneously after, that's why it doesn't increment the COUNT(*) value?
What other workarounds can I do? I cannot create a SEQUENCE because the SEQUENCE_ID must be based on the number of PRIMARY_ID that are present. They are both primary ids.
Thanks in advance.
Yes, The SELECT will be executed FIRST and only then the INSERT happens.
A Simple PL/SQL block below, will be a simpler approach, though not efficient.
DECLARE
V_SEQUENCE_ID NUMBER;
V_COMMIT_LIMIT:= 20000;
V_ITEM_COUNT := 0;
BEGIN
FOR REC IN (SELECT LEGACY_ID,DESCR FROM LEGACY_TABLE)
LOOP
V_SEQUENCE_ID:= 0;
SELECT COUNT(*)+1 INTO V_SEQUENCE_ID FROM TABLE_1 T1
WHERE T1.PRIMARY_ID = REC.LEGACY_ID
INSERT INTO TABLE_1
(PRIMARY_ID, SEQUENCE_ID, DESCR)
VALUES
(REC.LEGACY_ID,V_SEQUENCE_ID,REC.DESCR);
V_ITEM_COUNT := V_ITEM_COUNT + 1;
IF(V_ITEM_COUNT >= V_COMMIT_LIMIT)
THEN
COMMIT;
V_ITEM_COUNT := 0;
END IF;
END LOOP;
COMMIT;
END;
/
EDIT: Using CTE:
WITH TABLE_1_PRIM_CT(PRIMARY_ID, SEQUENCE_ID) AS
(
SELECT L1.LEGACY_ID,COUNT(*)
FROM LEGACY_TABLE L1
LEFT OUTER JOIN TABLE_1 T1
ON(L1.LEGACY_ID = T1.PRIMARY_ID)
GROUP BY L1.LEGACY_ID
)
INSERT INTO TABLE_1
(SELECT L1.LEGACY_ID,
CTE.SEQUENCE_ID+ (ROW_NUMBER() OVER (PARTITION BY L1.LEGACY_ID ORDER BY null)),
L1.DESCR
FROM TABLE_1_PRIM_CT CTE, LEGACY_TABLE L1
WHERE L1.LEGACY_ID = CTE.PRIMARY_ID);
PS: With your Millions of Rows, this is going to create a temp table
of same size, during execution. Do Explain Plan before actual execution.

Need to write a procedure to fetch given rownums

I need to write one procedure to pick the record for given rows
for example
procedure test1
(
start_ind number,
end_ind number,
p_out ref cursor
)
begin
opecn p_out for
select * from test where rownum between start_ind and end_ind;
end;
when we pass start_ind 1 and end_ind 10 its working.But when we change start_ind to 5
then query looks like
select * from test where rownum between 5 and 10;
and its fails and not shows the output.
Please assist how to fix this issue.Thanks!
The rownum is assigned and then the where condition evaluated. Since you'll never have a rownum 1-4 in your result set, you never get to rownum 5. You need something like this:
SELECT * FROM (
SELECT rownum AS rn, t.*
FROM (
SELECT t.*
FROM test t
ORDER BY t.whatever
)
WHERE ROWNUM <= 10
)
WHERE rn >= 5
You'll also want an order by clause in the inner select, or which rows you get will be undefined.
This article by Tom Kyte pretty much tells you everything you need to know: http://www.oracle.com/technetwork/issue-archive/2006/06-sep/o56asktom-086197.html
SELECT *
from (SELECT rownum AS rn, t.*
FROM MyTable t
WHERE ROWNUM <= 10
ORDER BY t.NOT-Whatever
-- (its highly important to use primary or unique key of MyTable)
WHERE rn > 5
As a hint, :
Typically we use store-procedures for data validation, access control, extensive or complex processing that requires execution of several SQL statements. Stored procedures may return result sets, i.e. the results of a SELECT statement. Such result sets can be processed using cursors, by other stored procedures, by associating a result set locator, or by applications
I think you are going to use the ruw-number to fetch paged queries.
Try to create a generic select query based on the idea mentioned above.
Two possibilities:
1) Your table is an index-organized table. So its data is sorted. You would select those first rows you want to avoid and based on that get the next rows you are looking for:
create or replace procedure get_records
(
vi_start_ind integer,
vi_end_ind integer,
vo_cursor out sys_refcursor
) as
begin
open vo_cursor for
select *
from test
where rownum <= vi_end_ind - vi_start_ind + 1
and rowid not in
(
select rowid
from test
where rownum < vi_start_ind
)
;
end;
2) Your table is not index-organized, which is normally the case. Then its records are not sorted. To get records m to n, you would have to tell the system what order you have in mind:
create or replace procedure get_records
(
vi_start_ind number,
vi_end_ind number,
vo_cursor out sys_refcursor
) as
begin
open vo_cursor for
select *
from test
where rownum <= vi_end_ind - vi_start_ind + 1
and rowid not in
(
select rowid from
(
select rowid
from test
order by somthing
)
where rownum < vi_start_ind
)
order by something
;
end;
All this said, think it over what you want to achieve. If you want to use this procedure to read your table block for block, keep in mind that it will read the same data again and again. To know what rows 1,000,001 to 1,000,100 are, the dbms must read through one million rows first.

Resources