update using for loop in plsql - oracle

i'm having problem updating and insert into below column. Please advise on this.
This is the input
depnto extra comm
----------------------------
20 300 NULL
20 300 400
20 NULL NULL
20 500 NULL
This is the expected output
depnto Extra comm
---------------------
20 300 300
20 300 400
20 NULL NULL
20 500 500
I need to update comm column with extra column on below conditions.
If comm Is null then extra value is updated to comm.
If comm Is not null, no need to update,
If both are null, leave as null,
if comm column has a value no need to overwrite.
My program is below. Even I need to keep track which are rows are updated and to which value in another table.
PROCEDURE (dept_id )
AS
BEGIN
FOR r IN (SELECT *
FROM emp
WHERE comm IS NULL AND extra IS NOT NULL AND deptno = dept_id)
LOOP
UPDATE emp
SET comm = extra
WHERE comm IS NULL AND extra IS NOT NULL AND deptno = dept_id;
INSERT INTO changed_comm (deptno, oldval, newval)
VALUES (dept_id, r.comm, r.extra);
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
NULL;
END;
please provide some opinion on above. Its not inserting correctly.

You do not need FOR LOOP, just a single UPDATE does the work:
UPDATE emp
SET comm = extra
WHERE comm IS NULL AND extra IS NOT NULL;
Here is a demo: http://www.sqlfiddle.com/#!4/aacc3/1
--- EDIT ----
I didn't notice, that in the expected output deptno 10 was updated to 20, to update deptno an another query is needed:
UPDATE emp
SET deptno = 20
WHERE deptno = 10;
---- EDIT -----
If you want to insert changed values to the other table, try a procedure with RETURNING..BULK COLLECT and FORALL:
CREATE OR REPLACE PROCEDURE pro_cedure( p_dept_id number )
IS
TYPE changed_table_type IS TABLE OF changed%ROWTYPE;
changed_buff changed_table_type;
BEGIN
SELECT deptno, comm, extra BULK COLLECT INTO changed_buff
FROM emp
WHERE comm IS NULL AND extra IS NOT NULL AND deptno = p_dept_id
FOR UPDATE;
UPDATE emp
SET comm = extra
WHERE comm IS NULL AND extra IS NOT NULL AND deptno = p_dept_id;
FORALL i IN 1 .. changed_buff.count
INSERT INTO changed VALUES changed_buff( i );
END;
/
The procedure should work if you are not going to process huge number of records in a one call (more than 1000 ... or maximum a few thousands). If one dept_id can contain ten thousands and more rows, then this procedure might be slow, becasue it will consume a huge amount of PGA memory. In such a case, an another approach with bulk collectiong in chunks is required.
-- EDIT --- how to store sequence values -------
I assume that the table changed has 4 columns, like this:
CREATE TABLE "TEST"."CHANGED"
( "DEPTNO" NUMBER,
"OLDVAL" NUMBER,
"NEWVAL" NUMBER,
"SEQ_NEXTVAL" NUMBER
) ;
and we will store sequence values in the seq_nextval column.
In such a case the procedure might look like this:
create or replace
PROCEDURE pro_cedure( p_dept_id number )
IS
TYPE changed_table_type IS TABLE OF changed%ROWTYPE;
changed_buff changed_table_type;
BEGIN
SELECT deptno, comm, extra, sequence_name.nextval
BULK COLLECT INTO changed_buff
FROM emp
WHERE comm IS NULL AND extra IS NOT NULL AND deptno = p_dept_id
FOR UPDATE;
UPDATE emp
SET comm = extra
WHERE comm IS NULL AND extra IS NOT NULL AND deptno = p_dept_id;
FORALL i IN 1 .. changed_buff.count
INSERT INTO changed VALUES changed_buff( i );
END;
--- EDIT --- version with cursor for small sets of data -----
Yes, for small sets of data bulk collecting doesn't give significant increase of the speed, and plain cursor with for..loop is sufficient in such a case.
Below is an example how tu use the cursor together with update, notice the FOR UPDATE clause, it is required when we plan to update a record fetched from the cursor using WHERE CURRENT OF clause.
This time a sequence value is evaluated within the INSERT statement.
create or replace
PROCEDURE pro_cedure( p_dept_id number )
IS
CURSOR mycursor IS
SELECT deptno, comm, extra
FROM emp
WHERE comm IS NULL AND extra IS NOT NULL
AND deptno = p_dept_id
FOR UPDATE;
BEGIN
FOR emp_rec IN mycursor
LOOP
UPDATE emp
SET comm = extra
WHERE CURRENT OF mycursor;
INSERT INTO changed( deptno, oldval, newval, seq_nextval)
VALUES( emp_rec.deptno, emp_rec.comm,
emp_rec.extra, sequence_name.nextval );
END LOOP;
END;

BEGIN
FOR person IN (SELECT A FROM EMP WHERE B IN (SELECT B FROM ustom.cfd_180518) )
LOOP
--dbms_output.put_line(person.A);
UPDATE custom.cfd_180518 SET c = person.a;
END LOOP;
END;

Related

How can I catch an insert on a table statement in plsql , calculate a unique value based on input column names and column values

I have to capture an insert statement's column names and column values , based on these 2 inputs I need to calculate a unique column value(sub-partition) and assign it to this insert so that the insert goes into a particular sub-partition of this table.
I'm not sure what you mean by "capturing" the INSERT statement.
From my point of view, you should compose it (the INSERT) yourself, using dynamic SQL.
My database is XE and it doesn't support partitioning, so I'd rather not guess it and not being able to test it; you'd, of course, use your own syntax. I'm using CASE to decide whether someone's COMM columns should get a value or not; you'd do it similarly.
Procedure:
SQL> create or replace procedure p_test
2 (p_empno in varchar2,
3 p_ename in varchar2,
4 p_sal in number)
5 is
6 l_str varchar2(200);
7 begin
8 l_str := 'insert into test (empno, ename, sal, comm) ' ||
9 'values (' ||
10 p_empno ||', '||
11 chr(39) || p_ename || chr(39) || ', ' ||
12 p_sal ||', '||
13 case when p_sal < 1000 then 100 else 0 end ||')';
14 dbms_output.put_line(l_str);
15 execute immediate l_str;
16 end;
17 /
Procedure created.
Testing:
SQL> set serveroutput on;
SQL> exec p_test(1, 'Little', 750);
insert into test (empno, ename, sal, comm) values (1, 'Little', 750, 100)
PL/SQL procedure successfully completed.
SQL> exec p_test(1, 'Foot', 2000);
insert into test (empno, ename, sal, comm) values (1, 'Foot', 2000, 0)
PL/SQL procedure successfully completed.
SQL> select * From test;
EMPNO ENAME SAL COMM
---------- ---------- ---------- ----------
1 Little 750 100
1 Foot 2000 0
SQL>
#Littlefoot , Thanks as in insert into abc ('name','empid','partition_id') values ('nparab',2000,10); The value of partition_id=10 is not known it will be calculated based on name and empid combination...I even dont knw the incoming insert statement , can i use trigger to catch the incoming insert then I can pass the column name and column value to let say a function and that function based on input combination of column name and column value calculates the partition_id and the function returns partition_id which is picked by the incoming insert so that insert statement goes in respective subpartition based on the partition_id

trigger to check avg salary of the department before inserting new record

lets say employee table is as below
EID ENAME DEPTNO SALARY
1 john 10 100
2 jau 10 300
3 cau 10 200
4 cha 20 200
5 cwea 20 500
6 dan 20 200
7 an 20 300
I have to check if any new employee is added, the new employee salary should be greater than the average salary in that department, and this should be done in triggers.
so I have created trigger as below
create or replace trigger tg_emp before insert on employee for each row
declare
avgsal number;
highsalary EXCEPTION;
BEGIN
select avg(salary) into avgsal from employee where deptno = :NEW.deptno;
if :NEW.salary < avgsal
then
raise highsalary;
end if;
EXCEPTION
when highsalary then
Raise_Application_Error (-20343, 'salary is less than the avg salary in this
department');
WHEN others THEN
Raise_Application_Error (-20353, 'other error probably table mutation
error');
END;
as you know with this code it works for only individual inserts like below
insert into employee values (8, 'jj', 10, 500);
but if it is a multiple inserts at once like
insert into employee
select seq_emp.next, 'ffgg', 10, 400 from all_tab_columns where rownum < 5;
it throws table mutation error(I know the above insert does not make sense but I am using it as just an example for multi insert in one statement).
so how can we resolve this using global temporary tables?
I think I was able to solve it using 1 GTT and 1 before statement trigger and 1 before row trigger as below
CREATE GLOBAL TEMPORARY TABLE employee_GTT (
id NUMBER,
name VARCHAR2(20),
deptno number,
salary number
)
ON COMMIT DELETE ROWS;
statement level before trigger
create or replace trigger emp_avg_load before insert on employee
begin
insert into dept_avg
select deptno, avg(salary), count(deptno) from employee group by deptno;
dbms_output.put_line('getting data from GTT');
end;
row level before trigger
create or replace trigger tg_emp before insert on employee for each row
declare
avgsal number;
ct number;
highsalary EXCEPTION;
BEGIN
avgsal := :new.salary;
select avgsal, count into avgsal, ct from dept_avg where deptno =
:NEW.deptno;
if :NEW.salary < avgsal
then
raise highsalary;
else
update dept_avg
set count = count +1,
avgsal = (avgsal+:NEW.salary)/(count+1)
where deptno = :NEW.deptno;
end if;
EXCEPTION
when highsalary then
Raise_Application_Error (-20343, 'salary is less than the avg salary in this
department');
WHEN others THEN
Raise_Application_Error (-21343, 'some other error');
END;
Please correct me if I get it wrong.
The way you use the temporary table is good idea at first.
But the way you update the average salary during the transaction looks wrong to me. Indeed, depending on how Oracle handles the update (order of inserts), you won't have the same results.
First, since you only insert employee when salary is above average, then average can only increase.
Now , if highest salary is inserted first, then average might increase too much for the next employee to be inserted.
You are having difficulties because your requirement isn't clear enough.
I do not think it is real use case to change the average salary while you are inserting the rows. You must think of it at a transaction level: thus, average salary is defined before transaction happens. So no need to have the average change while you are inserting. Just let the average stay the same during transaction.
I would remove this part:
else
update dept_avg
set count = count +1
, avgsal = (avgsal+:NEW.salary)/(count+1)
where deptno = :NEW.deptno;

PL/SQL_procedure to raise emp salary has errors

The question is to write a block of codes that for those employees with a salary less than 1100, raise those people's salary by 50.
My code is as below but has error:
ERROR at line 2: Encountered the symbol "RAISE_SALARY" when expecting one of the following: := . ( # % ; immediate The symbol "; was inserted before "RAISE_SALARY" to continue.
CREATE OR REPLACE procedure raise_salary
AS
CURSOR c1 IS
select ename, sal from emp
where sal <1100
order by sal ASC;
BEGIN
FOR emp_rec IN c1
LOOP
UPDATE emp
SET sal = sal + 50;
END LOOP;
END;
/
begin
execute raise_salary;
end;
/
Search a little further, I think you'll find that the procedure didn't update the salary by 150, but actually updated by 50 the number of times equal to the number of rows returned in the cursor. In this case 3 rows (so 3 * 50 = 150) for anyone you validate. Further, it updated all salaries not only the ones in the cursor.
The update statement does not contain a WHERE clause, thus every row in the table is updated.
Further a simple update statement will accomplish what you need:
Update emp
set sal = sal + 50
where sal < 1100;
Get into the habit of thinking in terms of sets instead individual rows.

How to use sum() function inside stored procedure in oracle?

The example below works fine and it return some rows. But I need summary of the rows.
DECLARE
x number;
Cursor c1 is
select sal,deptno from emp;
rw c1%rowtype;
BEGIN
x:=0;
open c1;
LOOP
fetch c1 into rw;
FOR i IN 1..rw.deptno LOOP
x:=x+rw.sal;
end loop;
exit when c1%notfound;
DBMS_OUTPUT.PUT_LINE(x);
END LOOP;
close c1;
END;
/
Suppose you have three employees and every employee's has different salary. The salary has due for 10 months and 20 months and 30 months. The salary is due for long time. So you want to add 2% bonus amount with salary for every month as the way:
The below description is for single Employee for 10 months:
Month-1 Salary = 800 => 800*2% = 16.00 => Total = 800+16 =816
Month-2 Salary = 816 => 816*2% = 16.32 => Total = 816+16.32 =832.32
............................................................................
Month-10 Salary = 956.07 => 956.07*% = 19.12 => Total = 956.07+19.12 =975.20
The Months-1 Total Salary=816. So Month-2 Salary=816. This will continue up 10 months.Every Employee has the same condition. So I need summary of the total column. Thanks and best regards.
When you use aggregate function SUM in your query (unlike, when you adding yourself), you don't need to convert NULL. SUM takes care of it. Although, as #DavidAldridge pointed, if you expect that all rows in summarized group of records may contain NULL, your sum will also be NULL. If you want to return a value, you can wrap your sum as follows coalesce(sum(sal),0)
This will give you SUM of all salaries
select SUM(sal) TotalSal from emp;
This will give you SUM by department
select SUM(sal) TotalDeptSal, deptno
from emp
group by deptno;
In you question you posted that you need to execute it in stored procedure while your code as an anonymous block. If you want to return single value from Stored procedure you have a choice to declare function with return parameter or stored procedure with output parameter. To return a recordset from stored procedure in Oracle you need to declare a refcursor output parameter
CREATE OR REPLACE PROCEDURE Get_TotalSal_ByDept (
p_recordset OUT SYS_REFCURSOR) AS
BEGIN
OPEN p_recordset FOR
select SUM(sal) TotalDeptSal, deptno
from emp
group by deptno;
END;
Edit
I see that you added row - total. It is not changing much from the original question. Still, using cursor is not needed. You can run 2 queries and return 2 output parameters, one with data by department and another is total.
CREATE OR REPLACE PROCEDURE Get_SalByDept_WithTotal (
p_total OUT NUMBER,
p_recordset OUT SYS_REFCURSOR) AS
BEGIN
select SUM(sal) INTO p_total from emp;
OPEN p_recordset FOR
select SUM(sal) TotalDeptSal, deptno
from emp
group by deptno;
END;
Is this what you looking for? The Running totals?
SELECT totals.deptNo, totals.depttotal, SUM(totals.depttotal) OVER (ORDER BY totals.id)
FROM (
select deptNo, deptTotal, rownum id
from (
select deptNo, sum(sal * deptNo) deptTotal
from emp
group by deptNo)
) totals
ORDER BY totals.id;
If you have some sort of department Id you can use that instead of artificially generated one from ROWNUM

Oracle update lock

I am in bit confusion regarding update lock, in the below program
CREATE OR REPLACE
PROCEDURE pro_cedure(
p_dept_id NUMBER )
IS
CURSOR mycursor
IS
SELECT deptno,comm,extra
FROM emp
WHERE comm IS NULL
AND extra IS NOT NULL
AND deptno = p_dept_id
FOR UPDATE OF comm;
BEGIN
FOR emp_rec IN mycursor
LOOP
UPDATE emp SET comm = extra
WHERE CURRENT OF mycursor;
INSERT INTO changed
(
deptno,
oldval,
newval,
seq_nextval
)
VALUES
(
emp_rec.deptno,
emp_rec.comm,
emp_rec.extra,
sequence_name.nextval
);
END LOOP;
END;
if some other user try to update the same table or the same row while I am updating does it causes a lock with this program?
If you run this procedure 2 times parallel, the second wont execute the select until the first commits. (Of course if you run other statements with the same for update of comm clause they will be queued the same way.)

Resources