I have executed below block in SQL*PLUS and it successfully executed the whole block.
Strangely, it inserted only 32768 unique values in the table and remaining 7232 were duplicates.
Please note we have to manually call the local procedure 40k times without using a loop.
/*
-- #Hello
-- please create this test table
-- on your database schema
CREATE TABLE my_table (
num NUMBER,
upd_dt DATE DEFAULT SYSDATE
)
/
*/
DECLARE
TYPE my_tp IS TABLE OF my_table%ROWTYPE;
my_nest_t my_tp := my_tp ();
PROCEDURE add (
p_num IN NUMBER
)
AS
BEGIN
my_nest_t.EXTEND;
my_nest_t (my_nest_t.LAST) := NULL;
my_nest_t (my_nest_t.LAST).num := p_num;
END add;
BEGIN
add (1);
add (2);
add (3);
add (4);
/*
-- #Hello
-- this way manually write the call to
-- the local proc 40000 times
-- I used CONNECT BY LEVEL to generate the script
-- SELECT ' add (p_num=>' || level || ');' FROM DUAL CONNECT BY LEVEL < 40001
-- use this query only to generate the 40k rows of code
*/
add (39999);
add (40000);
IF my_nest_t.COUNT < 40000 THEN
RAISE_APPLICATION_ERROR (-20001, '#Hello - please call PROCEDURE add 40000 times as mentioned above');
END IF;
FORALL i IN 1.. my_nest_t.COUNT
INSERT INTO my_table (num)
VALUES (my_nest_t (i).num);
COMMIT;
END;
/
/*
-- #Hello
*/
-- 40000, 32768
SELECT COUNT (1),
COUNT (DISTINCT num)
FROM my_table
Works fine for me on version 12.1.
I believe there are duplicates among calls to add procedure.
Here, how I generated those for tests
select 'add(' || level || ');' from dual connect by level <= 40000
then I copied them to the plsql block and launched. After that the check-query from you
SELECT COUNT (1), COUNT (DISTINCT num)
FROM ek_test;
displays 40000 for both coleumns.
So my answer is, please check all those 40 thousands of procedure calls and look for duplicates.
Related
I have a procedure in oracle that looks like this
create or replace procedure check_display_stock as
id_brg number(20);
rowcount number (20);
begin
-- 1. insert datas from display into temp_display
insert into temp_display
select id_barang,stok,min_stok
from display
where stok <= min_stok;
--2. select the number of datas in temp_display
select count(rownum)
into rowcount
from temp_display;
while(rowcount != 0)
loop
-- Error: no data found
select id_barang
into id_brg
from temp_display
where rownum = 1;
--just another procedure to do other things
insert_spb(id_brg);
delete from temp_display where rownum = 1;
end if;
end loop;
end check_display_stock;
An error occurs when I tried to select into that says no data found.
I don't understand why this happened.
You never decrement rowcount so you will end up deleting the rows in temp_display one-by-one and then keep going (potentially forever) and on the next iteration after you have already emptied the table you will try to select id_barang into id_brg ... and it will fail as you have already emptied the table.
Instead, you could use BULK COLLECT INTO or a CURSOR to bypass the temporary table:
create or replace procedure check_display_stock
as
begin
FOR cur IN ( select id_barang,
stok,
min_stok
from display
where stok <= min_stok
)
LOOP
insert_spb(cur.id_barang);
END LOOP;
END;
/
db<>fiddle here
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
What's the best way of getting and outputting how many rows have been inserted in the FORALL statement I have below. I've seen the SQL%BULK_ROWCOUNT but I'm not sure how that would work in the below statement.
is it
DBMS_OUTPUT.('rows inserted '||SQL%BULK_ROWCOUNT||'');
Does the above need to go in another FORALL statement? For the code below how would I achieve this?
DECLARE
TYPE t_arc_act_plus_trigger1 IS TABLE OF arc_act_plus_triggers1%ROWTYPE;
v_arc_act_plus_triggers1 t_arc_act_plus_trigger1;
CURSOR c_arc_act_plus_triggers1 IS
SELECT /*+ PARALLEL */ apt.*
FROM act_plus_triggers1 apt
WHERE NOT EXISTS
(SELECT 1
FROM act_plus_triggers_copy1 aptc
WHERE aptc.surr_id = apt.surr_id)
AND apt.status IN ('EXT', 'EXP');
BEGIN
OPEN c_arc_act_plus_triggers1;
LOOP
FETCH c_arc_act_plus_triggers1 BULK COLLECT INTO v_arc_act_plus_triggers1 LIMIT 10000; -- limit to 10k to avoid out of memory
FORALL i IN 1..v_arc_act_plus_triggers1.COUNT
INSERT /*+ APPEND_VALUES */ INTO arc_act_plus_triggers1 values v_arc_act_plus_triggers1(i);
Com0932.get_parameter ('ACT_ARCHIVE_TRIGGER_STOP_YN',l_STOP_PROGRAM_YN);
IF l_STOP_PROGRAM_YN = 'Y' THEN
p_location('insert_into_arc_act_plus - STOP_PROGRAM_YN flag = '||l_STOP_PROGRAM_YN||' so ROLLBACK');
ROLLBACK;
EXIT;
END IF;
-- **************************************************
-- Output how many records have been inserted here???
-- **************************************************
-- commit after every 10000 records into arc_act_plus_triggers1
COMMIT;
EXIT WHEN c_arc_act_plus_triggers1%NOTFOUND;
END LOOP;
CLOSE c_arc_act_plus_triggers1;
END;
I haven't checked as I have nothing to test against so please forgive any 'missing semi-colon type errors' and I'm afraid I'm not in a position to performance check this.
Your code seems to select which rows to insert to the archive table based on there non-existence in the archive. Therefore simply use an INSERT based on a SELECT limited by a suitable ROWNUM value. Once you commit then the next time round the loop it wont try getting already archived rows as you just committed them.
I think this should be as quick if not quicker than bulkifying the inserts with the advantage that its simpler - Occams Razor and all that.
DECLARE
l_commit_count NUMBER := 10000;
l_rows_copied NUMBER := 0;
BEGIN
DBMS_OUTPUT.PUT_LINE('Started at '||TO_DATE(SYSDATE, 'DD_MON_YYY HH24:MI:SS');
LOOP
INSERT /*+APPEND */
INTO c_arc_act_plus_triggers1
SELECT /*+ PARALLEL */ apt.*
FROM act_plus_triggers1 apt
WHERE NOT EXISTS
(SELECT 1
FROM act_plus_triggers_copy1 aptc
WHERE aptc.surr_id = apt.surr_id)
AND apt.status IN ('EXT', 'EXP')
AND rownum < l_commit_count;
COMMIT;
l_rows := l_rows + SQL%ROWCOUNT;
EXIT WHEN SQL%ROWCOUNT < 1;
END LOOP
DBMS_OUTPUT.PUT_LINE('Finished at '||TO_DATE(SYSDATE, 'DD_MON_YYY HH24:MI:SS');
DBMS_OUTPUT.PUT_LINE(TO_CHAR(l_rows)||' rows copied to the archive table');
END;
I am creating a dynamic procedure which could accept 2 table names.Fetch the records from one table and after certain record (let's say 100 records) i have to issue the commit command.
Both tabName and temp_tabName are always be identical.Since I have billions of records in first table i am doing the commit after every 10000 records in order to get rid of undo table space problem.
Till now what i did is :
CREATE OR REPLACE PROCEDURE MyProdecure (
tabName IN USER_TABLES.table_name%TYPE,
temp_tabName IN USER_TABLES.table_name%TYPE
)
IS
v_sql VARCHAR2 (100) := 'select * from ' || tabName;
TEMP_CURSOR SYS_REFCURSOR;
COUNT NUMBER (6) := 0;
BEGIN
OPEN TEMP_CURSOR FOR v_sql;
LOOP
FETCH TEMP_CURSOR INTO V_ROW;
--=================================================================================
/*
* I need the code here to fetch the 100 record from TEMP_CURSOR into a Variable
* and insert into the second table. or one record increment the count and if
* count>= 100 commit
*What would be the data type of V_ROW. How to fetch the data from V_ROW and complete the insert into command.
*/
--================================================================================
EXIT WHEN TEMP_CURSOR%NOTFOUND;
END LOOP;
CLOSE TEMP_CURSOR;
END MyProdecure;
There is no way to define V_ROW in such a way as to make your PL/SQL block work correctly for an input table whose name and structure is not known until runtime.
To make your approach work, you would need to use DBMS_SQL.
Have you considered a variation of the following, to bypass the vast majority of the UNDO generation?
CREATE OR REPLACE PROCEDURE MyProcedure (
tabName IN USER_TABLES.table_name%TYPE,
temp_tabName IN USER_TABLES.table_name%TYPE
)
IS
l_log_io NUMBER;
C_BLOCK_SIZE NUMBER := 8192; -- assuming 8192 byte block size
l_undo_bytes NUMBER;
BEGIN
EXECUTE IMMEDIATE 'INSERT /*+ APPEND */ INTO ' || temp_tabName ||
' SELECT * FROM ' || tabName;
select t.log_io, t.used_ublk*C_BLOCK_SIZE undo_bytes
into l_log_io, l_undo_bytes
from v$transaction t
where t.addr = ( SELECT s.taddr FROM v$session s WHERE s.sid = USERENV('SID'));
dbms_output.put_line('Undo bytes used: ' || l_undo_bytes);
END;
INSERT /*+ APPEND */ comes with a number of caveats that you should look into before using it, but it could be a much simpler way of accomplishing your goal.
I want to update the record if already exists based on where condition or else insert. I have written query with delete and insert I am facing difficulty into converting into update and insert.
PS: I have tried using SQL%ROWCOUNT but I think i missed somewhere in syntax.
Any help would be appreciated.
Below is the proc i am using
Create Or Replace Procedure sal_proc(Empid Varchar2,Fmdt Date,bp Number)
As
Begin
--delete
delete from Emp_Sal
Where Empid = Empid
And Fmdt = Fmdt;
--insert
Insert Into Emp_Sal(empid,fmdt,Basicpay) Values (empid,fmdt,Bp);
End;
You do not need a procedure; you can use MERGE to update or insert a row at the same time:
merge into Emp_Sal e
using (
/* your values to insert/update */
select 2 as Empid, 'c' as Fmdt, 100 as Bp from dual
) x
on ( e.Empid = x.Empid And e.Fmdt = x.Fmdt)
when matched
then /* if a record exists, update */
update set Basicpay = Bp
when not matched
then /* it the record does not exist, insert */
insert values (x.empid, x.fmdt, x.Bp)
Of course you can use this inside a procedure to handle your input parameters or do whatever you may need to do in your procedure:
Create Or Replace Procedure sal_proc(Empid Varchar2,Fmdt Date,bp Number)
As
Begin
merge into Emp_Sal e
using (
/* your values to insert/update */
select Empid as Empid, Fmdt as Fmdt, bp as Bp from dual
) x
on ( e.Empid = x.Empid And e.Fmdt = x.Fmdt)
when matched
then /* if a record exists, update */
update set Basicpay = Bp
when not matched
then /* it the record does not exist, insert */
insert (empid,fmdt,Basicpay) values (x.empid, x.fmdt, x.Bp);
End;
An hint: use parameter names different fron column names to avoid confusion; a good practice could be use parameter names like p_XXX; just an example of how dangerous this can be:
SQL> create or replace procedure checkPar(n in number) is
2 c number;
3 begin
4 select count(1)
5 into c
6 from checkTab
7 where n = n;
8 --
9 dbms_output.put_line(c);
10 end;
11 /
Procedure created.
SQL> select * from checkTab;
N
----------
1
2
3
SQL> exec checkPar(1);
3
PL/SQL procedure successfully completed.
SQL> exec checkPar(999);
3
PL/SQL procedure successfully completed.
Not very efficient way but should work if huge data is not present in tables
CREATE OR REPLACE PROCEDURE sal_proc (E_Empid VARCHAR2, F_Fmdt DATE, b_bp NUMBER)
AS
var number;
BEGIN
Begin
--Checking if record exists
select 1
into var
from Emp_Sal
Where Empid = E_Empid
And Fmdt = F_Fmdt;
exception
when no_data_found then
var:= 0;
End;
if var <> 1 then
--insert
INSERT INTO Emp_Sal (empid, fmdt, Basicpay)
VALUES (e_empid, f_fmdt, b_Bp);
else
update Emp_Sal
set col ....<>;
end if;
END;