How to use sum() function inside stored procedure in oracle? - 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

Related

How can I create an array in PL/SQL that takes values from a SELECT statement?

I'm trying to create a script that can select a list of values, put them into an array, then iterate the array so that the selected values can be deleted. I think I'm ultimately just confusing myself, but my biggest issue seems to be figuring out how to select values into an array. With every other example I've seen on here, people define the array statically, not with a select statement.
I've tried to create a FOR loop that looks like this:
DECLARE
type PERMIDS IS VARRAY(10) OF VARCHAR2(10);
ID PERMIDS;
total integer;
BEGIN
ID := PERMIDS('1','2','3','4','5');
total := ID.count;
FOR i in 1 .. total LOOP
SYS.DBMS_OUTPUT.PUT_LINE('ID: ' || ID(i));
END LOOP;
END;
This runs, but it doesn't allow for dynamic definition of the array.
What I'd like to do is something like this, with the select statement potentially yielding any number of results:
DECLARE
type PERMIDS IS VARRAY(10) OF VARCHAR2(10);
ID PERMIDS;
total integer;
BEGIN
SELECT rg.ID INTO ID
FROM table1 rg
LEFT JOIN table2 a
ON rg.ID = a.ID
WHERE a.ID = '1234';
total := ID.count;
FOR i in 1 .. total LOOP
SYS.DBMS_OUTPUT.PUT_LINE('ID: ' || ID(i));
END LOOP;
END;
It's important that this loop can handle any number of results from the select statement, 0 included, as a user could have any number of records.
One option is to use built-in sys.odcivarchar2list type and bulk collect into it.
Sample table (department names will be put into a collection):
SQL> select * from dept;
DEPTNO DNAME LOC
---------- -------------- -------------
10 ACCOUNTING NEW YORK
20 RESEARCH DALLAS
30 SALES CHICAGO
40 OPERATIONS BOSTON
PL/SQL procedure:
SQL> set serveroutput on
SQL>
SQL> declare
2 l_id sys.odcivarchar2list;
3 begin
4 select dname
5 bulk collect into l_id
6 from dept;
7
8 for i in 1 .. l_id.count loop
9 dbms_output.put_line('ID = ' || l_id(i));
10 end loop;
11 end;
12 /
ID = ACCOUNTING
ID = RESEARCH
ID = SALES
ID = OPERATIONS
PL/SQL procedure successfully completed.
SQL>

PL/SQL: ORA-00947: not enough values

Im creating a procedure to display the n number of maximum and minimum salary for an employee. If i ll give 5 as input, the query will get me 5 maximum and minimum salary for an employee.
For the above scenario, I have created an object with two columns like below
create type vrec as object(
empno number,
sal number
);
/
Then i created nested table with the help of object type, so that i can use the nested table as out parameter to return all the rows at one short.
create type vrec_type is table of vrec;
/
After the data type creation, im creating a procedure like below
create or replace procedure pro_first_last(input in number,salary out vrec_type)
is
begin
select empno,sal BULK COLLECT INTO salary from (
select empno,sal from
(select empno,sal,rank() over(order by sal asc) min_sal from emp5 where sal is not null) where min_sal <= 5
union all
select empno,sal from
(select empno,sal,rank() over(order by sal desc) max_sal from emp5 where sal is not null) where max_sal <= 5);
for i in salary.first..salary.last
loop
dbms_output.put_line(salary(i).empno);
end loop;
end;
/
When i compiling the above procedure, im getting not enough values. I have also created the object with two columns and in select statement also im returning only two column. Could someone review and help me on this or provide some alternate solution.
You are directly adding empno, sal values into salary (vrec_type object, which can take the values of only object type vrec)
You need to create the object of vrec and then add it into salary as following:
create or replace procedure pro_first_last(input in number,salary out vrec_type)
is
begin
select vrec(empno,sal) -- change in this line
BULK COLLECT INTO salary from (
select empno,sal from
(select empno,sal,rank() over(order by sal asc) min_sal from emp5 where sal is not null) where min_sal <= 5
union all
select empno,sal from
(select empno,sal,rank() over(order by sal desc) max_sal from emp5 where sal is not null) where max_sal <= 5);
for i in salary.first..salary.last
loop
dbms_output.put_line(salary(i).empno);
end loop;
end;
Cheers!!

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;

In Oracle PL/SQL, how do i iterate/access a collection containing the result of LISTAGG function

Assuming table EMP has three columns (deptno, ename and salary).
How would i write a for loop that would look like this and be able to access the items in the LISTAGG function
BEGIN
FOR rec IN (SELECT deptno, LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename) AS employees
FROM emp
WHERE deptNo = 'XYZ'
GROUP BY deptno)
LOOP
Update EMP
set salary = 10000
WHERE ename in (THE ITEMS IN THE LISTAGG)
FOR EACH (ITEM RETURNED IN THE LISTAGG)
DO SOMETHING
END;
END LOOP;
END;
Basically i want to know two things,
How can i refer to all the items in returned by the listagg function (See UPDATE statement inside for loop)
How can i iterate through each of the items returned by teh listagg function. See INNER FOR LOOP.
Would the syntax for the above also work for WM_CONCAT and COLLECT functions.
Thanks in advance.
Using PL/SQL ListAGG Function Results in a Procedural Loop
Here is a sample application of the OP's request:
BEGIN
FOR rec IN (SELECT deptno, LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename) AS employees
FROM emp
WHERE deptNo = 'XYZ'
GROUP BY deptno)
LOOP
Update EMP
set salary = 10000
WHERE ename in
( select regexp_substr(rec.employees,'[^,]+',1,level)
from dual
connect by level <= length(regexp_replace(rec.employees,'[^,]+')) + 1
)
AND deptNo=rec.deptNo; --not to miss this filter condition
FOR EACH (ITEM RETURNED IN THE LISTAGG)
DO SOMETHING
END;
END LOOP;
END;
To see another working example of parsing a delimited string list, see Stack Overflow for a similar problem involving a delimited string parsing routine.

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;

Resources