PL/SQL_procedure to raise emp salary has errors - oracle

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.

Related

PLSQL Updating employee salary where department_id = 30 using cursor

I need to update employee salary from department 30. all employees from department 30 will have 20% salary increase. I run into error while executing my code.
Here is my code:
DECLARE
CURSOR cur_emp
IS
SELECT * FROM employees WHERE department_id = 30;
rec_emp cur_emp%rowtype;
BEGIN
OPEN cur_emp;
LOOP
FETCH cur_emp INTO rec_emp;
IF rec_emp.department_id = 30 THEN
UPDATE employees
SET salary = salary + (salary * 0.20)
WHERE employee_id = rec_emp.employee_id;
END IF;
EXIT
WHEN cur_emp%notfound;
END LOOP;
CLOSE cur_emp;
END;
/
You can handle the task only by one DML statement rather than a code block, after commenting out the parts of the trigger's code which leads to the current issue or disabling it of course, such as
SQL> UPDATE employees
SET salary = ROUND(salary * 1.2,2)
WHERE department_id = 30;
/
SQL> COMMIT;

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;

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

update using for loop in plsql

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;

Compare two records and show only the difference

I'd like a Oracle Pipelined Function which compares two records from a query result and shows only the column which have changed between the query result?
Here is a blunt instrument approach to the problem.
create or replace function col_diff
( p_empno_1 in emp.empno%type
, p_empno_2 in emp.empno%type )
return col_nt pipelined
is
out_val col_t := new col_t(null, null, null);
emp_rec1 emp%rowtype;
emp_rec2 emp%rowtype;
begin
select *
into emp_rec1
from emp
where empno = p_empno_1;
select *
into emp_rec2
from emp
where empno = p_empno_2;
if emp_rec1.ename != emp_rec2.ename
then
out_val.col_name := 'ENAME';
out_val.old_val := emp_rec1.ename;
out_val.new_val := emp_rec2.ename;
pipe row (out_val);
end if;
if emp_rec1.hiredate != emp_rec2.hiredate
then
out_val.col_name := 'HIREDATE';
out_val.old_val := to_char(emp_rec1.hiredate, 'DD-MON-YYYY');
out_val.new_val := to_char(emp_rec2.hiredate, 'DD-MON-YYYY');
pipe row (out_val);
end if;
return;
end;
/
So, given this test data...
SQL> select empno, ename, hiredate
2 from emp
3 where empno > 8100
4 /
EMPNO ENAME HIREDATE
---------- ---------- ---------
8101 PSMITH 03-DEC-10
8102 PSMITH 02-JAN-11
SQL>
... we get this output:
SQL> select * from table (col_diff(8101,8102))
2 /
COL_NAME
------------------------------
OLD_VAL
-------------------------------------------------------------------
NEW_VAL
-------------------------------------------------------------------
HIREDATE
03-DEC-2010
02-JAN-2011
SQL>
Now, doubtlessly you would like something which is less verbose. I think it may be possible to do something using the enhanced Method 4 dynamic SQL which was introduced in 11g. Alas, you say you are using 10g.
Its not quite what you want, but Kevin Meade's blog on OracleFAQ has a solution which works for me:
http://www.orafaq.com/node/1826
http://www.orafaq.com/files/column_diffs.txt

Resources