How to Update value in one table by coping it from another table in plsql ? - oracle

This is a simple example of what I need to do. In fact I want to Update value in one table by coping it from another table by using cursors in plsql.
I take table f and table b as two examples:
f=
1|Thom
2|Bob
3|Steven
5|Arthur
b=
7|Nataly
9|Alfred
, where I need to insert the b 's tow lines in first f two lines:
create table f (a number, b varchar2(10));
insert into f values (1,'Thom');
insert into f values (2,'Bob');
insert into f values (3,'Steven');
insert into f values (5,'Arthur');
commit;
create table b (c number, d varchar2(10));
insert into b values (7,'Nataly');
insert into b values (9,'Alfred');
commit;
create or replace procedure wco as
cursor c_f is
select a,b from f for update;
v_a f.a%type;
v_b f.b%type;
cursor c_b is
select c,d from b;
v_c b.c%type;
v_d b.d%type;
begin
open c_f;
open c_b
loop
fetch c_f into v_a, v_b;
exit when c_f%ROWCOUNT=c_b%RROWCOUNT;
update f set a=v_c and b=v_d where current of c_f;
end loop;
close c_d:
close c_f;
end;
/
exec wco;
select * from f;
drop table f;
The expected result (What i hope to have):
7|Nataly
9|Alfred
3|Steven
5|Arthur
But what I have now (as a result)is:
1|Thom
2|Bob
3|Steven
5|Arthur
How do I resolve this problem, I am a beginner with PLSQL, I would be very grateful if you could help me please.

Your procedure compiles, but with errors;
In order to see compilation errors
run show errors; after compilation.
Change open c_b to open c_b; (missing semicolon at the end)
Change close c_d: to close c_b; (correct name is c_b and semicolon instead of colon)
Change exit when c_f%ROWCOUNT=c_b%RROWCOUNT; to exit when c_f%ROWCOUNT=c_b%ROWCOUNT; (wrong %RROWCOUNT;)
update f set a=v_c and b=v_d where current of c_f; This is wrong SQL syntax.
Should be update table_name set column=value, column=value;
It is important to provide descriptive names to your variables, so it would be easier to understand your logic.
As I understood you want to copy only second two rows from source table.
create table source_table (
id number,
name varchar2(10));
/
insert into source_table values (1,'Thom');
insert into source_table values (2,'Bob');
insert into source_table values (3,'Steven');
insert into source_table values (5,'Arthur');
/
create table target_table (
id number,
name varchar2(10));
/
insert into target_table values (7,'Nataly');
insert into target_table values (9,'Alfred');
/
create or replace procedure copy_tables
AS
begin
FOR source_row IN (select id, name
from source_table
offset 2 rows)
LOOP
insert into target_table
values(source_row.id, source_row.name);
END LOOP;
end;
/
exec copy_tables;
/
select id, name from target_table;
/

You don't need PL/SQL for this; it can be achieved in a single MERGE statement:
merge into f tgt
using (select coalesce(b1.id, f1.id) id,
coalesce(b1.name, f1.name) name,
f1.f_rowid
from (select id,
name,
rowid f_rowid,
row_number() over (order by id) rn
from f) f1
full outer join (select id,
name,
row_number() over (order by id) rn
from b) b1
on f1.rn = b1.rn) src
on (tgt.rowid = src.f_rowid)
when matched then
update set tgt.id = src.id,
tgt.name = src.name
where tgt.id != src.id
or tgt.name != src.name
when not matched then
insert (tgt.id, tgt.name)
values (src.id, src.name);
See https://livesql.oracle.com/apex/livesql/file/content_FBPR7YCLFVWO7NGDXTLSP1R97.html for the test details (two examples; the target table has more and has fewer rows than the source table).
If you need to, you could add the above insert into a procedure, e.g.:
create procedure populate_target_table as
begin
merge into f tgt
using (select coalesce(b1.id, f1.id) id,
coalesce(b1.name, f1.name) name,
f1.f_rowid
from (select id,
name,
rowid f_rowid,
row_number() over (order by id) rn
from f) f1
full outer join (select id,
name,
row_number() over (order by id) rn
from b) b1
on f1.rn = b1.rn) src
on (tgt.rowid = src.f_rowid)
when matched then
update set tgt.id = src.id,
tgt.name = src.name
where tgt.id != src.id
or tgt.name != src.name
when not matched then
insert (tgt.id, tgt.name)
values (src.id, src.name);
end;
/

Related

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.

Insert into select query with cursor value

I want to write a SQK script with insert query in Oracle where one of the value will be fetched from cursor and rest all will be retrieved from table.
For example, consider Employee table:
Emp_No | Emp_Name
1 | AAA
...........
I am reading the table into a cursor.
Cursor c1 is select emp_no, emp_name from employee;
I am iterating the cursor and adding to a table along with information from another table.
for empCur in c1
loop
insert into employee_info(emp_no, emp_name, address, age, ... ) values (empCur.emp_no, empCur.emp_name, select t.address, t.age, ... from employee_temp_table t where t.emp_no=empCur.emp_no)
end loop;
Is my script valid? If not is there any other way to achieve it? Since few values are in cursor and few are in another table I am not sure how to handle this.
Your script isn't correct because this
select t.address, t.age, ... from employee_temp_table t where t.emp_no=empCur.emp_no
is not a valid expression. You may use a scalar subquery (one-row, one-column subquery) only, like this:
insert into t1(col1, col2, col3) values (1, (select col1 from t2), (select col2 from t2));
Or you may try insert from select:
for empCur in c1 loop
insert into employee_info(emp_no, emp_name, address, age, ... )
select empCur.emp_no, empCur.emp_name, t.address, t.age, ... from employee_temp_table t where t.emp_no=empCur.emp_no;
end loop;
If you want to use the cursor, why not just join the tables inside the cursor?
for empCur in ( select e.emp_no, e.emp_name, t.address, t.age ...
from employee e join employee_temp_table t on ( t.emp_no = e.emp_no )
) loop
insert into employee_info(...) values ( empCur.emp_no, ...);
end loop;
Or with a sql insert: (if you can choose sql over pl/sql - T Kyte says do it)
insert into employee_info
select e.emp_no, e.emp_name, t.address, t.age ...
from employee e join employee_temp_table t on ( t.emp_no = e.emp_no );

Oracle: how to INSERT if a row doesn't exist

What is the easiest way to INSERT a row if it doesn't exist, in PL/SQL (oracle)?
I want something like:
IF NOT EXISTS (SELECT * FROM table WHERE name = 'jonny') THEN
INSERT INTO table VALUES ("jonny", null);
END IF;
But it's not working.
Note: this table has 2 fields, say, name and age. But only name is PK.
INSERT INTO table
SELECT 'jonny', NULL
FROM dual -- Not Oracle? No need for dual, drop that line
WHERE NOT EXISTS (SELECT NULL -- canonical way, but you can select
-- anything as EXISTS only checks existence
FROM table
WHERE name = 'jonny'
)
Assuming you are on 10g, you can also use the MERGE statement. This allows you to insert the row if it doesn't exist and ignore the row if it does exist. People tend to think of MERGE when they want to do an "upsert" (INSERT if the row doesn't exist and UPDATE if the row does exist) but the UPDATE part is optional now so it can also be used here.
SQL> create table foo (
2 name varchar2(10) primary key,
3 age number
4 );
Table created.
SQL> ed
Wrote file afiedt.buf
1 merge into foo a
2 using (select 'johnny' name, null age from dual) b
3 on (a.name = b.name)
4 when not matched then
5 insert( name, age)
6* values( b.name, b.age)
SQL> /
1 row merged.
SQL> /
0 rows merged.
SQL> select * from foo;
NAME AGE
---------- ----------
johnny
If name is a PK, then just insert and catch the error. The reason to do this rather than any check is that it will work even with multiple clients inserting at the same time. If you check and then insert, you have to hold a lock during that time, or expect the error anyway.
The code for this would be something like
BEGIN
INSERT INTO table( name, age )
VALUES( 'johnny', null );
EXCEPTION
WHEN dup_val_on_index
THEN
NULL; -- Intentionally ignore duplicates
END;
I found the examples a bit tricky to follow for the situation where you want to ensure a row exists in the destination table (especially when you have two columns as the primary key), but the primary key might not exist there at all so there's nothing to select.
This is what worked for me:
MERGE INTO table1 D
USING (
-- These are the row(s) you want to insert.
SELECT
'val1' AS FIELD_A,
'val2' AS FIELD_B
FROM DUAL
) S ON (
-- This is the criteria to find the above row(s) in the
-- destination table. S refers to the rows in the SELECT
-- statement above, D refers to the destination table.
D.FIELD_A = S.FIELD_A
AND D.FIELD_B = S.FIELD_B
)
-- This is the INSERT statement to run for each row that
-- doesn't exist in the destination table.
WHEN NOT MATCHED THEN INSERT (
FIELD_A,
FIELD_B,
FIELD_C
) VALUES (
S.FIELD_A,
S.FIELD_B,
'val3'
)
The key points are:
The SELECT statement inside the USING block must always return rows. If there are no rows returned from this query, no rows will be inserted or updated. Here I select from DUAL so there will always be exactly one row.
The ON condition is what sets the criteria for matching rows. If ON does not have a match then the INSERT statement is run.
You can also add a WHEN MATCHED THEN UPDATE clause if you want more control over the updates too.
Using parts of #benoit answer, I will use this:
DECLARE
varTmp NUMBER:=0;
BEGIN
-- checks
SELECT nvl((SELECT 1 FROM table WHERE name = 'john'), 0) INTO varTmp FROM dual;
-- insert
IF (varTmp = 1) THEN
INSERT INTO table (john, null)
END IF;
END;
Sorry for I don't use any full given answer, but I need IF check because my code is much more complex than this example table with name and age fields. I need a very clear code. Well thanks, I learned a lot! I'll accept #benoit answer.
In addition to the perfect and valid answers given so far, there is also the ignore_row_on_dupkey_index hint you might want to use:
create table tq84_a (
name varchar2 (20) primary key,
age number
);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Johnny', 77);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Pete' , 28);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Sue' , 35);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Johnny', null);
select * from tq84_a;
The hint is described on Tahiti.
you can use this syntax:
INSERT INTO table_name ( name, age )
select 'jonny', 18 from dual
where not exists(select 1 from table_name where name = 'jonny');
if its open an pop for asking as "enter substitution variable" then use this before the above queries:
set define off;
INSERT INTO table_name ( name, age )
select 'jonny', 18 from dual
where not exists(select 1 from table_name where name = 'jonny');
You should use Merge:
For example:
MERGE INTO employees e
USING (SELECT * FROM hr_records WHERE start_date > ADD_MONTHS(SYSDATE, -1)) h
ON (e.id = h.emp_id)
WHEN MATCHED THEN
UPDATE SET e.address = h.address
WHEN NOT MATCHED THEN
INSERT (id, address)
VALUES (h.emp_id, h.address);
or
MERGE INTO employees e
USING hr_records h
ON (e.id = h.emp_id)
WHEN MATCHED THEN
UPDATE SET e.address = h.address
WHEN NOT MATCHED THEN
INSERT (id, address)
VALUES (h.emp_id, h.address);
https://oracle-base.com/articles/9i/merge-statement
CTE and only CTE :-)
just throw out extra stuff. Here is almost complete and verbose form for all cases of life. And you can use any concise form.
INSERT INTO reports r
(r.id, r.name, r.key, r.param)
--
-- Invoke this script from "WITH" to the end (";")
-- to debug and see prepared values.
WITH
-- Some new data to add.
newData AS(
SELECT 'Name 1' name, 'key_new_1' key FROM DUAL
UNION SELECT 'Name 2' NAME, 'key_new_2' key FROM DUAL
UNION SELECT 'Name 3' NAME, 'key_new_3' key FROM DUAL
),
-- Any single row for copying with each new row from "newData",
-- if you will of course.
copyData AS(
SELECT r.*
FROM reports r
WHERE r.key = 'key_existing'
-- ! Prevent more than one row to return.
AND FALSE -- do something here for than!
),
-- Last used ID from the "reports" table (it depends on your case).
-- (not going to work with concurrent transactions)
maxId AS (SELECT MAX(id) AS id FROM reports),
--
-- Some construction of all data for insertion.
SELECT maxId.id + ROWNUM, newData.name, newData.key, copyData.param
FROM copyData
-- matrix multiplication :)
-- (or a recursion if you're imperative coder)
CROSS JOIN newData
CROSS JOIN maxId
--
-- Let's prevent re-insertion.
WHERE NOT EXISTS (
SELECT 1 FROM reports rs
WHERE rs.name IN(
SELECT name FROM newData
));
I call it "IF NOT EXISTS" on steroids. So, this helps me and I mostly do so.

Resources