Issue with BULK COLLECT of million rows - out of process memory - oracle

I have below code in my pl/sql procedure, which is trying to collect 17 millions rows, and it fails with error ORA-04030: out of process memory when trying to allocate 16328 bytes (koh-kghu call ,pmuccst: adt/record)
TYPE rc_test IS RECORD(
ROWID VARCHAR2(200),
contact_id VARCHAR2(200),
last_name VARCHAR2(200),
first_name VARCHAR2(200),
phone VARCHAR2(200),
email VARCHAR2(200),
birth_day date,
address_id VARCHAR2(200),
seq NUMBER,
NEWID NUMBER);
TYPE rctype
IS TABLE OF RC_TEST;
rcrecords RCTYPE;
BEGIN
SELECT ROWID,
contact_id,
last_name,
first_name,
phone,
email,
birth_day,
address_id,
seq,
NEWID
bulk collect INTO rcrecords
FROM HR.TMP_TBL_SEQ a
order by last_name, first_name, seq;
FOR i IN 1..rcrecords.count LOOP
-- <<>>
END LOOP;
It runs fine with sample data of hundreds or thousand of rows but doesn't work with millions of rows.
I have read troubleshooting for this error but most of them revolve around getting this done through DBA who will perform some tunings or will advise us to increase RAM that might resolve the issue but due to certain limitations at first place I really want to change the logic of the code itself.
Is there a better way to improve the logic in the above type of code to get rid of the out of memory error?
In place of table record used in my code can anyone provide me alternate code?
Many Thanks.

You can use BULK COLLECT with LIMIT clause (http://www.oracle.com/technetwork/issue-archive/2008/08-mar/o28plsql-095155.html). In your case it will look similar to :
CURSOR cur1 IS
SELECT ROWID, contact_id, last_name, first_name,
phone, email, birth_day, address_id, seq,
NEWID FROM HR.TMP_TBL_SEQ a order by last_name, first_name, seq;
TYPE T_TEST IS TABLE OF cur1 %ROWTYPE INDEX BY PLS_INTEGER;;
test_data T_TEST;
BEGIN
OPEN cur1;
LOOP
FETCH cur1
BULK COLLECT INTO test_data LIMIT 10000;
FOR indx IN 1 .. test_data.COUNT
LOOP
NULL;
--- process data
END LOOP;
EXIT WHEN test_data.COUNT < 10000;
END LOOP;
CLOSE cur1;

Avoid this sort of "unlimited" use of BULK COLLECT by using the LIMIT clause.
Move the SELECT statement into an explicit cursor declaration and then use a simple loop to fetch many but not all the rows from the table with each execution of the loop body using the optional LIMIT clause.
For an examples and explanations see this oracle tech-issue, see http://www.oracle.com/technetwork/issue-archive/2008/08-mar/o28plsql-095155.html

Replace the BULK COLLECT with a cursor for-loop:
begin
for rcrecords in
(
SELECT ROWID,
contact_id,
last_name,
first_name,
phone,
email,
birth_day,
address_id,
seq,
NEWID
bulk collect INTO rcrecords
FROM HR.TMP_TBL_SEQ a
order by last_name, first_name, seq
) loop
--Process data.
null;
end loop;
end;
/
Oracle will automatically take care of bulk collect, limit, and declaring the the data types.

Related

ORA-00947:not enough values

guys i am getting error that PL/SQL: ORA-00947: not enough values while running my code.
declare
type e_type is record ( last_name employees.last_name%type,
email employees.email%type,
hire_date employees.hire_date%type,
job_id employees.job_id%type);
type e_list is table of e_type index by pls_integer;
emps e_list;
begin
for x in 100 .. 110 loop
select last_name,email,hire_date,job_id into emps(x) from employees
where employee_id = x ;
---dbms_output.put_line(emps(x).email);
insert into emp(last_name,email,hire_date,job_id) values emps(x);
end loop;
end;
#WilliamRobertson presents an interesting solution (I had actually forgotten that format). However there is still a much simpler solution. That is to use a single sql statement for the select and insert while avoiding the type and variable declarations and the loop altogether:
insert into emp(last_name,email,hire_date,job_id)
select last_name,email,hire_date,job_id
from employees
where employee_id between 100 and 110;
The problem is here:
insert into emp(last_name,email,hire_date,job_id)
values emp(x);
The PL/SQL INSERT Statement Extension where you use a record variable for the values clause does not accept a column list, so it needs to be either
insert into emp values emp(x);
where emp(x) evaluates to a record matching the entire emp row, or else
insert into
( select last_name, email, hire_date, job_id from emp )
values emp(x);
where emp(x) is a record exactly matching the select list.

pl sql insert into within a procedure and dynamic variables

I need some help with PL SQL. I have to insert some data into table. Another application is calling my procedure and caller is passing few details which I also need to insert into my table.
here is the syntax I am struggling with:
PROCEDURE invform_last2orders_item_insert( p_userId IN NUMBER
,p_accountId IN NUMBER
,p_site_Id IN NUMBER
,p_return_message OUT VARCHAR2) IS
Begin
insert into mytable
(p_userId , p_accountId , p_site_Id , sku, description, 'Cart', 1, unitId)
as
select sku, description, unitId
from mycatalogtable where site_id= p_site_Id ) ;
End;
Can you help me with syntax? I need to pass three parameters from called in parameter and some values returned from select query. How can I achieve this?
thank you for your help.
That would be something like this; see comments within code:
PROCEDURE invform_last2orders_item_insert
( p_userId IN NUMBER
,p_accountId IN NUMBER
,p_site_Id IN NUMBER
,p_return_message OUT VARCHAR2)
IS
Begin
insert into mytable
-- first name all columns you'll be inserting into; I don't know their
-- names so I just guessed
(userid,
accountid,
siteid,
sku,
description,
col1,
col2,
unitid
)
-- if you were to insert only values you got via parameters, you'd use the
-- VALUE keyword and insert those values separately.
-- As some of them belong to a table, use SELECT statement
(select p_userid,
p_accountid,
p_siteid,
c.sku,
c.description,
'Cart',
1,
c.unitid
from mycatalogtable c
where c.site_id = p_site_Id
);
-- I don't know what you are supposed to return; this is just an example
p_return_message := sql%rowcount || ' row(s) inserted';
End;
in your select statement you should have the same number of columns as you are inserting into the table, your code should be something like this example,
DECLARE
userid varchar2(20) := 'Jack';
Begin
INSERT INTO mytable (SELECT userid, SPORT from OLYM.OLYM_SPORTS);
commit;
end;

PLSQL Count and Procedure

Currently trying to create a PL/SQL procedure. I am a complete noob at PL/SQL, as you can tell!
We have had to create a table using SQL, and we are looking to automatically update the table with a procedure. If the customer has requested more than 4 jobs, we are looking to input their details into this table as a frequent customer.
I currently have at the moment:
CREATE TABLE PublisherDetails
(PublisherName VARCHAR2 (40),
City VARCHAR2 (20) ,
PhoneNo NUMBER (11),
jobNo NUMBER (10),
startDate DATE,
completionDate DATE)
;
SELECT Publisher.Name AS PublisherName,
Publisher.City, Publisher.PhoneNo,
COUNT (*) AS PublisherJobCount
FROM Publisher
INNER JOIN PrintJob
ON Publisher.Name = PrintJob.PublisherName
GROUP BY Publisher.Name, Publisher.City, Publisher.PhoneNo;
Create or replace procedure Task3
IS CountPublisherJobs NUMBER;
DECLARE No_data_Found EXCEPTION
BEGIN
SELECT count(*) INTO CountPublisherJobs
OPEN Task3;
LOOP
IF PublisherJobCount < 3
THEN INSERT INTO PublisherDetails (PublisherName, City, PhoneNo)
FROM Publisher
WHERE PublisherName = publisher.name
Else
Insert Into PublisherDetails (JobNo, StartDate, CompletionDate )
SELECT jobNo, startDate, completionDate
FROM PrintJob
Where PublisherName = publishers.name
FETCH Task3 INTO PublisherDetails, publishername, city, phoneNo;
EXIT WHEN c1%NOTFOUND;
INSERT INTO temp VALUES (PublisherName, City, PhoneNo, JobNo, StartDate, CompletionDate);
END IF;
COMMIT;
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('Sorry no data found');
END;
/
Its churning up errors and I am not sure why. Any help as always is appreciated.
There are a number of things incorrect with the syntax of your procedure.
The basic format for a stored procedure is
Create {Or Replace} Procedure PROCEDURE_NAME {(i_param IN datatype)}
Is
<<Declaration Section>>
Begin
<<code section>>
Exception
<<Exceptions>>
End PROCEDURE_NAME;
From what you have described above, you want to insert a record into a table, when a condition is met in another table.
To accomplish this, I would need to see the underlying data structure, what you have provided doesn't show the tables the data is currently in (is there a JOB table for instance? a Customer table?).
The NO_DATA_FOUND exception does not need to be declared, it is an Oracle exception
Your Select Count(*) Into CountJobs is missing a From TABLE, and any predicates you want to add, although I am not sure what you are trying to accomplish with this.
You are attempting to open a cursor on the procedure name. You have not defined a cursor with the name Task3
You have not declared the CountPublisherJobs variable
I would suggest perhaps revisiting the basic structure for a stored procedure.
Edit
Based on your response, you could achieve the result using the following:
Create Or Replace Procedure addFrequentPublisher
Is
Cursor frequentPublishers Is
Select PUBLISHER_ID
From JOB
Group By
PUBLISHER_ID
Having Count(*) >= 4;
Begin
For i In frequentPublishers
Loop
Insert Into FREQUENT_CUSTOMER ...
End Loop;
End;

Bulk inserting in Oracle PL/SQL

I have around 5 million of records which needs to be copied from table of one schema to table of another schema(in the same database). I have prepared a script but it gives me the below error.
ORA-06502: PL/SQL: numeric or value error: Bulk bind: Error in define
Following is my script
DECLARE
TYPE tA IS TABLE OF varchar2(10) INDEX BY PLS_INTEGER;
TYPE tB IS TABLE OF SchemaA.TableA.band%TYPE INDEX BY PLS_INTEGER;
TYPE tD IS TABLE OF SchemaA.TableA.start_date%TYPE INDEX BY PLS_INTEGER;
TYPE tE IS TABLE OF SchemaA.TableA.end_date%TYPE INDEX BY PLS_INTEGER;
rA tA;
rB tB;
rD tD;
rE tE;
f number :=0;
BEGIN
SELECT col1||col2||col3 as main_col, band, effective_start_date as start_date, effective_end_date as end_date
BULK COLLECT INTO rA, rB, rD, rE
FROM schemab.tableb;
FORALL i IN rA.FIRST..rE.LAST
insert into SchemaA.TableA(main_col, BAND, user_type, START_DATE, END_DATE, roll_no)
values(rA(i), rB(i), 'C', rD(i), rE(i), 71);
f:=f+1;
if (f=10000) then
commit;
end if;
end;
Could you please help me in finding where the error lies?
Why not a simple
insert into SchemaA.TableA (main_col, BAND, user_type, START_DATE, END_DATE, roll_no)
SELECT col1||col2||col3 as main_col, band, 'C', effective_start_date, effective_end_date, 71
FROM schemab.tableb;
This
f:=f+1;
if (f=10000) then
commit;
end if;
does not make any sense. f becomes 1 - that's it. f=10000 will never be true, thus you don't make a COMMIT.
Following script worked for me and i was able to load around 5 millions of data within 15 minutes.
ALTER SESSION ENABLE PARALLEL DML
/
DECLARE
cursor c_p1 is
SELECT col1||col2||col3 as main_col, band, effective_start_date as start_date, effective_end_date as end_date
FROM schemab.tableb;
TYPE TY_P1_FULL is table of c_p1%rowtype
index by pls_integer;
v_P1_FULL TY_P1_FULL;
v_seq_num number;
BEGIN
open c_p1;
loop
fetch c_p1 BULK COLLECT INTO v_P1_FULL LIMIT 10000;
exit when v_P1_FULL.count = 0;
FOR i IN 1..v_P1_FULL.COUNT loop
INSERT /*+ APPEND */ INTO schemaA.tableA VALUES (v_P1_FULL(i));
end loop;
commit;
end loop;
close c_P1;
dbms_output.put_line('Load completed');
end;
-- Disable parallel mode for this session
ALTER SESSION DISABLE PARALLEL DML
/
ORA-06502: PL/SQL: numeric or value error: Bulk bind: Error in define
You get that error because you have a literal in the VALUES clause of the INSERT. The FORALL expects everything to be bind to an array.
Your program has a massive problem, literally. You have no LIMIT on the BULK COLLECT clause, so that's going to try to load all five million records from TableB into your collections. That will blow your session's memory limit.
The point of using BULK COLLECT and FORALL is to bite off chunks of a bigger data set and process it in batches. For that you need a loop. The loop has no FOR condition: instead test whether the fetch returned anything and exit when the array has zero records.
DECLARE
TYPE recA IS RECORD (
main_col SchemaA.TableA.main_col%TYPE
, band SchemaA.TableA.band%TYPE
, start_date date
, end_date date
, roll_ni number);
TYPE recsA is table of recA
nt_a recsA;
f number :=0;
CURSOR cur_b is
SELECT col1||col2||col3 as main_col,
band,
effective_start_date as start_date,
effective_end_date as end_date ,
71 as roll_no
FROM schemab.tableb;
BEGIN
open cur_b;
loop
fetch curb_b bulk collect into nt_a limit 1000;
exit when nt_a.count() = 0;
FORALL i IN rA.FIRST..rE.LAST
insert into SchemaA.TableA(main_col, BAND, user_type, START_DATE, END_DATE, roll_no)
values nt_a(i);
f := f + sql%rowcount;
if (f > = 10000) then
commit;
f := 0;
end if;
end loop;
commit;
close cur_b;
end;
Please note that issuing commits inside a loop is contraindicated. You lay yourself open to runtime errors such as ORA-01002 and ORA-01555. If your program does crash half-way through you will have great difficulty in resuming it without problems. By all means persist if you have problems with UNDO tablespace, but the correct answer is to get the DBA to enlarge the UNDO tablespace not weaken your code.
"i am using bulk insert because it gives better performance"
It is true that BULK COLLECT and FORALL ... INSERT is more performative than a CURSOR FOR loop with row-by-row single inserts. It is not more efficient than a pure SQL INSERT INTO ... SELECT. The value of the construct is that it allows us to manipulate the contents of the array before inserting it. This is handle if we have complex business rules which can only be applied programmatically.
Please try after changing first 2 line of your code with below:
DECLARE
TYPE tA IS TABLE OF SchemaA.TableA.main_col%TYPE INDEX BY PLS_INTEGER;
...
...
This may be because of data type/length mismatch. In declaration section you have missed to declare one to inherit type from table.
Also as mentioned, f logic for commit will not do the magic for you. Better you should use LIMIT with BULL COLLECT

Iterate over a column in PL/SQL

I have a table Emp with EmpID, Empname, Salary and I am trying to do a calculation for each employee. But I am having problems trying to iterate over each emp to do the calculation. I cant use explicit cursors though.
So right now I am just trying to create the list of empIDs:
Declare
aRows Number;
eid emp_ID%TYPE;
Begin
Select Count(*)
Into aRows
from emp;
Select emp_ID
Into eid
From emp;
FOR days IN 1..Tot_Rows
Loop
Dbms_Output.Put_Line(eid);
eid := eid + 1;
End Loop;
END;
But I get the error:
PLS-00320: the declaration of the type of this expression is incomplete or malformed
The simplest way to iterate over the rows in a table in PL/SQL is to do something like
BEGIN
FOR employees IN (SELECT emp_id FROM emp)
LOOP
dbms_output.put_line( employees.emp_id );
END LOOP;
END;
Alternately, you could fetch all the EID values into a PL/SQL collection and iterate over the collection, as in this example
DECLARE
TYPE emp_id_tbl IS TABLE OF emp.emp_id%type;
l_emp_ids emp_id_tbl ;
BEGIN
SELECT emp_id
BULK COLLECT INTO l_emp_ids
FROM emp;
FOR i IN l_emp_ids .FIRST .. l_empnos.LAST
LOOP
dbms_output.put_line( l_emp_ids (i) );
END LOOP;
END;
If your query can return thousands of rows, however, fetching all the data into the collection may use more of the PGA memory than you'd like and you may need to fetch rows in chunks using the LIMIT clause. But that would seem to be getting ahead of ourselves at this point.
Justin Cave has explained how to do it, but to specifically look at the error you got, that was because of this:
eid emp_ID%TYPE;
When using the %TYPE you have to specify the table name as well as the column name:
eid emp.emp_ID%TYPE;
If you were selecting all the columns in the row you could also look at %ROWTYPE.
Your approach was also making two assumptions: that the initial select into eid found the lowest ID, which is by no means guaranteed; and that all the subsequent ID values are sequential. And you're declaring and populating aRows but referring to Tot_Rows.

Resources