How to assign value to oracle cursor while iterating - oracle

I have Oracle cursor which contains list of employee table records,
which have column values employee_name, salary, city, state where salary is null.
i want to add salary for each employee based on city or state during iteration of cursor.
e.g.
if city is X and state is Y salary := 10000
if city is null and state is Y salary :=2000
if state is null and city is Z salary :=10001
This part i don't have any issue
I want to populate the cursor which salary during the iteration time.
so once my loop is over I need that cursor with populated salary;
I have tried many things but not able to achieve I am not able to provide any code which I tried.
It will be great if someone can help me on it.
Or it will be great i can have new cursor and can assign the value when iterating the first one.
Code which I tried
for EMPLOYEE_RECORD in EMPLOYEE_RECORD_CUR (COUNTRY_CODE)
LOOP
--update salary based on if else
EMPLOYEE_RECORD.SALARY:=45454464646;
end loop;
for EMPLOYEE_RECORD in EMPLOYEE_RECORD_CUR (COUNTRY_CODE)
LOOP
--checking whether salary has been populated or not
-- i know this is wrong approach but need help on it
dbms_output.put_line( EMPLOYEE_RECORD.SALARY);
end LOOP;

You can consider using oracle collections, in your case, you need to have a collection(array) of records. To do it, refer to the ff codes.Put this on your declaration part:
TYPE myrecordtype is RECORD(employee_name VARCHAR2(32767),
salary NUMBER,
city VARCHAR2(32767),
state VARCHAR2(32767));
TYPE collectiontype is table of myrecordtype index by varchar2(32767);
mycollection collectiontype;
then inside your loop, add the following:
mycollection(EMPLOYEE_RECORD.employee_name).salary := --number based on your salary computation;
mycollection(EMPLOYEE_RECORD.employee_name).city := EMPLOYEE_RECORD.city;
mycollection(EMPLOYEE_RECORD.employee_name).employee_name := EMPLOYEE_RECORD.employee_name;
mycollection(EMPLOYEE_RECORD.employee_name).state := EMPLOYEE_RECORD.state;
then outside that loop, you can refer to the salary on that array based on the employee name(in other words, you can "query" that collection/array based on employee_names). It goes like this:
mycollection(employee_name).salary;
or if you want to loop through the collection, you can do this:
for EMPLOYEE_RECORD in EMPLOYEE_RECORD_CUR
loop
dbms_output.put_line(mycollection(EMPLOYEE_RECORD.employee_name).salary);
end loop;

Related

INCONSISTENCY UPDATING all salaries of a table

I'm trying to increase all salaries of the employee table with the following procedure.
The problem is that the salary column is doing something weird, is working like a variable, because is saving the salary of the last employee in the cursor and adding up to the next employee, so, at the end I got an error
ORA-01438: value larger than specified precision allowed for this column
PROCEDURE increase_salaries AS
v_emp NUMBER;
v_sal NUMBER;
BEGIN
FOR r1 IN cur_emps LOOP
v_emp := r1.employee_id;
v_sal := r1.salary;
UPDATE employees_copy
SET
salary = salary + salary;
COMMIT;
-- salary = salary + salary * v_salary_increase_rate;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
print('Error in employee '||v_emp);
END increase_salaries;
Thanks
I knwo that I can use first a SELECT INTO for the actual salary and re-initialized it to 0, but I saw many examples on internet using UPDATE salary = salary + ... and it works but with my code does not work.
If you have a table with:
CREATE TABLE employees_copy (
salary NUMBER(6,2)
);
Then you can put values from -9999.99 to 9999.99 into the column. If you try to set a value that is outside those bounds then you will get the error:
ORA-01438: value larger than specified precision allowed for this column
You can either:
Use a smaller value; or
Increase the precision of the column using:
ALTER TABLE employees_copy MODIFY (salary NUMBER(7,2));
and then you can put values from -99999.99 to 99999.99 into the column.
However, you probably also need to fix your procedure to only update a single employee at each loop iteration (rather than updating all employees every loop iteration):
PROCEDURE increase_salaries AS
BEGIN
FOR r1 IN cur_emps LOOP
UPDATE employees_copy
SET salary = 2 * salary;
WHERE employee_id = r1.employee_id;
END LOOP;
END increase_salaries;
/
Note 1: Don't catch the OTHERS exception. Let the exception happen and then you can debug it.
Note 2: Don't COMMIT in the procedure; if you do then you cannot chain together multiple procedures and ROLLBACK them all if one fails. Instead, COMMIT from wherever you are calling the procedure.

PL/SQL : Need to compare data for every field in a table in plsql

I need to create a procedure which will take collection as an input and compare the data with staging table data row by row for every field (approx 50 columns).
Business logic :
whenever a staging table column value will mismatch with the corresponding collection variable value then i need to update 'FAIL' into staging table STATUS column and reason into REASON column for that row.
If matched then need to update 'SUCCESS' in STATUS column.
Payload will be approx 500 rows in each call.
I have created below sample script:
PKG Specification :
CREATE OR REPLACE
PACKAGE process_data
IS
TYPE pass_data_rec
IS
record
(
p_eid employee.eid%type,
p_ename employee.ename%type,
p_salary employee.salary%type,
p_dept employee.dept%type
);
type p_data_tab IS TABLE OF pass_data_rec INDEX BY binary_integer;
PROCEDURE comp_data(inpt_data IN p_data_tab);
END;
PKG Body:
CREATE OR REPLACE
PACKAGE body process_data
IS
PROCEDURE comp_data (inpt_data IN p_data_tab)
IS
status VARCHAR2(10);
reason VARCHAR2(1000);
cnt1 NUMBER;
v_eid employee_copy.eid%type;
v_ename employee_copy.ename%type;
BEGIN
FOR i IN 1..inpt_data.count
LOOP
SELECT ec1.eid,ec1.ename,COUNT(*) over () INTO v_eid,v_ename,cnt1
FROM employee_copy ec1
WHERE ec1.eid = inpt_data(i).p_eid;
IF cnt1 > 0 THEN
IF (v_eid=inpt_data(i).p_eid AND v_ename = inpt_data(i).p_ename) THEN
UPDATE employee_copy SET status = 'SUCCESS' WHERE eid = inpt_data(i).p_eid;
ELSE
UPDATE employee_copy SET status = 'FAIL' WHERE eid = inpt_data(i).p_eid;
END IF;
ELSE
NULL;
END IF;
END LOOP;
COMMIT;
status :='success';
EXCEPTION
WHEN OTHERS THEN
status:= 'fail';
--reason:=sqlerrm;
END;
END;
But in this approach i have below mentioned issues.
Need to declare all local variables for each column value.
Need to compare all variable data using 'and' operator. Not sure whether it is correct way or not because if there are 50 columns then if condition will become very heavy.
IF (v_eid=inpt_data(i).p_eid AND v_ename = inpt_data(i).p_ename) THEN
Need to update REASON column when any column data mismatched (first mismatched column name) for that row, in this approach i am not able to achieve.
Please suggest any other good way to achieve this requirement.
Edit :
There is only one table at my end i.e target table. Input will come from any other source as collection object.
REVISED Answer
You could load the the records into t temp table, but unless you want additional processing it's not necessary. AFAIK there is no way to identify the offending column (first one only) without slugging through column-by-column. However, your other concern having to declare a variable is not necessary. You can declare a single variable defined as %rowtype which gives you access to each column by name.
Looping through an array of data to find the occasional error is just bad (imho) with SQL available to eliminate the good ones in one fell swoop. And it's available here. Even though your input is a array we can use as a table by using the TABLE operator, which allows an array (collection) as though it were a database table. So the MINUS operator can till be employed. The following routine will set the appropriate status and identify the first miss matched column for each entry in the input array. It reverts to your original definition in package spec, but replaces the comp_data procedure.
create or replace package body process_data
is
procedure comp_data (inpt_data in p_data_tab)
is
-- define local array to hold status and reason for ecah.
type status_reason_r is record
( eid employee_copy.eid%type
, status employee_copy.status%type
, reason employee_copy.reason%type
);
type status_reason_t is
table of status_reason_r
index by pls_integer;
status_reason status_reason_t := status_reason_t();
-- define error array to contain the eid for each that have a mismatched column
type error_eids_t is table of employee_copy.eid%type ;
error_eids error_eids_t;
current_matched_indx pls_integer;
/*
Helper function to identify 1st mismatched column in error row.
Here is where we slug our way through each column to find the first column
value mismatch. Note: There is actually validate the column sequence, but
for purpose here we'll proceed in the input data type definition.
*/
function identify_mismatch_column(matched_indx_in pls_integer)
return varchar2
is
employee_copy_row employee_copy%rowtype;
mismatched_column employee_copy.reason%type;
begin
select *
into employee_copy_row
from employee_copy
where employee_copy.eid = inpt_data(matched_indx_in).p_eid;
-- now begins the task of finding the mismatched column.
if employee_copy_row.ename != inpt_data(matched_indx_in).p_ename
then
mismatched_column := 'employee_copy.ename';
elsif employee_copy_row.salary != inpt_data(matched_indx_in).p_salary
then
mismatched_column := 'employee_copy.salary';
elsif employee_copy_row.dept != inpt_data(matched_indx_in).p_dept
then
mismatched_column := 'employee_copy.dept';
-- elsif continue until ALL columns tested
end if;
return mismatched_column;
exception
-- NO_DATA_FOUND is the one error that cannot actually be reported in the customer_copy table.
-- It occurs when an eid exista in the input data but does not exist in customer_copy.
when NO_DATA_FOUND
then
dbms_output.put_line( 'Employee (eid)='
|| inpt_data(matched_indx_in).p_eid
|| ' does not exist in employee_copy table.'
);
return 'employee_copy.eid ID is NOT in table';
end identify_mismatch_column;
/*
Helper function to find specified eid in the initial inpt_data array
Since the resulting array of mismatching eid derive from a select without sort
there is no guarantee the index values actually match. Nor can we sort to build
the error array, as there is no way to know the order of eid in the initial array.
The following helper identifies the index value in the input array for the specified
eid in error.
*/
function match_indx(eid_in employee_copy.eid%type)
return pls_integer
is
l_at pls_integer := 1;
l_searching boolean := true;
begin
while l_at <= inpt_data.count
loop
exit when eid_in = inpt_data(l_at).p_eid;
l_at := l_at + 1;
end loop;
if l_at > inpt_data.count
then
raise_application_error( -20199, 'Internal error: Find index for ' || eid_in ||' not found');
end if;
return l_at;
end match_indx;
-- Main
begin
-- initialize status table for each input enter
-- additionally this results is a status_reason table in a 1:1 with the input array.
for i in 1..inpt_data.count
loop
status_reason(i).eid := inpt_data(i).p_eid;
status_reason(i).status :='SUCCESS';
end loop;
/*
We can assume the majority of data in the input array is valid meaning the columns match.
We'll eliminate all value rows by selecting each and then MINUSing those that do match on
each column. To accomplish this cast the input with TABLE function allowing it's use in SQL.
Following produces an array of eids that have at least 1 column mismatch.
*/
select p_eid
bulk collect into error_eids
from (select p_eid, p_ename, p_salary, p_dept from TABLE(inpt_data)
minus
select eid, ename, salary, dept from employee_copy
) exs;
/*
The error_eids array now contains the eid for each miss matched data item.
Mark the status as failed, then begin the long hard process of identifying
the first column causing the mismatch.
The following loop used the nested functions to slug the way through.
This keeps the main line logic clear.
*/
for i in 1 .. error_eids.count -- if all inpt_data rows match then count is 0, we bypass the enttire loop
loop
current_matched_indx := match_indx(error_eids(i));
status_reason(current_matched_indx).status := 'FAIL';
status_reason(current_matched_indx).reason := identify_mismatch_column(current_matched_indx);
end loop;
-- update employee_copy with appropriate status for each row in the input data.
-- Except for any cid that is in the error eid table but doesn't exist in the customer_copy table.
forall i in inpt_data.first .. inpt_data.last
update employee_copy
set status = status_reason(i).status
, reason = status_reason(i).reason
where eid = inpt_data(i).p_eid;
end comp_data;
end process_data;
There are a couple other techniques used you may want to look into if you are not familiar with them:
Nested Functions. There are 2 functions defined and used in the procedure.
Bulk Processing. That is Bulk Collect and Forall.
Good Luck.
ORIGINAL Answer
It is NOT necessary to compare each column nor build a string by concatenating. As you indicated comparing 50 columns becomes pretty heavy. So let the DBMS do most of the lifting. Using the MINUS operator does exactly what you need.
... the MINUS operator, which returns only unique rows returned by the
first query but not by the second.
Using that this task needs only 2 Updates: 1 to mark "fail", and 1 to mark "success". So try:
create table e( e_id integer
, col1 varchar2(20)
, col2 varchar2(20)
);
create table stage ( e_id integer
, col1 varchar2(20)
, col2 varchar2(20)
, status varchar2(20)
, reason varchar2(20)
);
-- create package spec and body
create or replace package process_data
is
procedure comp_data;
end process_data;
create or replace package body process_data
is
package body process_data
procedure comp_data
is
begin
update stage
set status='failed'
, reason='No matching e row'
where e_id in ( select e_id
from (select e_id, col1, col2 from stage
except
select e_id, col1, col2 from e
) exs
);
update stage
set status='success'
where status is null;
end comp_data;
end process_data;
-- test
-- populate tables
insert into e(e_id, col1, col2)
select (1,'ABC','def') from dual union all
select (2,'No','Not any') from dual union all
select (3,'ok', 'best ever') from dual union all
select (4,'xx','zzzzzz') from dual;
insert into stage(e_id, col1, col2)
select (1,'ABC','def') from dual union all
select (2,'No','Not any more') from dual union all
select (4,'yy', 'zzzzzz') from dual union all
select (5,'no e','nnnnn') from dual;
-- run procedure
begin
process_data.comp_date;
end;
-- check results
select * from stage;
Don't ask. Yes, you to must list every column you wish compared in each of the queries involved in the MINUS operation.
I know the documentation link is old (10gR2), but actually finding Oracle documentation is a royal pain. But the MINUS operator still functions the same in 19c;

SQL trigger to stop update when a condition is met

I have 3 tables: Projects, Components, and Suppliers.
What I am trying to do is writing a trigger that doesn't allow the value of city to be modified if the component and the project have the same city as the supplier.
What I have tried so far:
create or replace TRIGGER Supplier_control
BEFORE UPDATE of city
ON Suppliers
BEGIN
DECLARE v_counter NUMBER := 0;
SELECT COUNT(*) FROM (SELECT * FROM Suppliers s JOIN Projects p ON (s.city=p.city) JOIN Components c ON (c.city=s.city)) INTO v_counter;
IF (v_counter != 0)
THEN
raise_application_error(-20111,'Can't change the city for this supplier!');
END IF;
END;
After trying to run this, I am getting the following error:
Error at line 3: PLS-00103: Encountered the symbol "JOIN" when expecting one of the following:
) , with group having intersect minus order start union where
connect
Please note that the line number refers to the number of the line after BEGIN!
I have also tried writing the declare part before BEGIN, I am getting the following error:
Error at line 3: PL/SQL: SQL Statement ignored
What needs to be done in order to get rid of these errors?
There are some syntax errors.
DECLARE goes before the BEGIN statement.
INTO goes after SELECT and before FROM.
At raise_application_error(-20111,'Can't change the city for this supplier!'); you cannot write Can't because the first single quote will end at the quote of Can't causing the string to end there. So you should remove it or do: raise_application_error(-20111,'Can''t change the city for this supplier!');
With all that being said, the full code should look like:
CREATE OR REPLACE TRIGGER Supplier_control
BEFORE UPDATE of city
ON Suppliers
DECLARE
v_counter NUMBER := 0;
BEGIN
SELECT COUNT(*)
INTO v_counter
FROM (SELECT * FROM Suppliers s JOIN Projects p ON s.city=p.city JOIN Components c ON c.city=s.city);
IF v_counter != 0 THEN
raise_application_error(-20111,'Can''t change the city for this supplier!');
END IF;
END;
Hope this helps.
You are trying to access the variable which is assigned as zero in the declaration.
set the variable from the query (result of this will be a number)
create or replace TRIGGER Supplier_control
BEFORE UPDATE of city
ON Suppliers
BEGIN
DECLARE v_counter NUMBER := 0;
-- change this line
SET v_counter = (SELECT COUNT(*) FROM (SELECT * FROM Suppliers s JOIN Projects p ON (s.city=p.city) JOIN Components c ON (c.city=s.city)));
IF (v_counter != 0)
THEN
raise_application_error(-20111,'Can't change the city for this supplier!');
END IF;
END;
changed the count query to the edited one. First simply run the query SELECT COUNT(*) FROM (SELECT * FROM Suppliers s JOIN Projects p ON (s.city=p.city) JOIN Components c ON (c.city=s.city)) if the output is number then assign to the variable v_counter
There is no need for the overhead of a Join nor even transfer of any date. Assuming City is indexed in Projects and Components (probably should be an indexed FK), the following requires only a simple index prob on each table.
create or replace trigger supplier_control
before update of city
on suppliers
for each row
declare
project_component_exists integer ;
begin
select null
into project_component_exists
from dual
where exists ( select null
from projects
where city = :old.city
)
and exists ( select null
from components
where city = :old.city
);
raise_application_error(-20111,'Can''t change the city for this supplier!');
exception
when no_data_found
then null;
end;
Check ,by lower() or upper() to match including case-insensitivity, whether a city match throughout the whole table Project's values.
You don't need to and shouldn't use Supplier table in the select statement due to risk of table name is mutating error :
CREATE OR REPLACE TRIGGER Supplier_control
BEFORE UPDATE of city
ON Suppliers
DECLARE
v_counter PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_counter
FROM Projects p
WHERE lower(:new.city)=lower(p.city);
IF (v_counter != 0) THEN
raise_application_error(-20111,'Can''t change the city for this supplier!');
END IF;
END;
where an initialization is redundant for v_counter, if there's no matching record found it would already be zero. Moreover, notice the order of declare keyword including variable definition part with it.

Code not working. It takes an eternity to run

The code below runs for an eternity.
As you can see i have to take values from one table and use that value to check if the second table contains it or not and insert into the third table values from the first table.
Is there any other way of doing this?
create or replace PROCEDURE KPI_AVAILABILITY (
v_programid varchar2
)
AS
v_MASTER_KPI_ID number;
v_UDF varchar2(100);
v_count number;
cursor c1 is
(select MASTER_KPI_ID,UDF from KPI_MASTER
where UDF is not null
and ISACTIVE = 1
--order by MASTER_KPI_ID,udf
);
BEGIN
open c1 ;
fetch c1 into v_MASTER_KPI_ID,v_UDF;
while v_UDF is not null
loop
select count(v_UDF) into v_count
from vw_ticket
where v_UDF is not null
and amsprogramid = v_programid;
if v_count is not null or v_count <> 0 then
delete from program_kpi where amsprogramid = v_programid;
INSERT INTO PROGRAM_KPI (AMSPROGRAMID,MASTER_KPI_ID,LASTUPDATEDBYDATALOAD)
VALUES(V_PROGRAMID,v_MASTER_KPI_ID,to_char(sysdate,'dd-mon-yy hh.mi.ss'));
dbms_output.put_line('xyz');
end if;
end loop;
close c1;
END KPI_AVAILABILITY;
Reverse engineering business rules from another developer's code is always tricky, especially without understanding the wider domain. However, at the centre of the loop is DELETE from program_kpi followed by an INSERT into the same table. If there are no records matching on amsprogramid = v_programid then you're inserting a record, if there are matches then effectively you're just updating lastupdatedbydataload with the current SYSDATE.
In others, it appears to be the logic of a MERGE. So perhaps your code could be entirely replaced with a single statement. If so, this is likely to be a lot more efficient than the row-by-agonizing-row process within a cursor loop.
merge into program_kpi pkpi
using (select kpim.master_kpi_id
, kpim.udf
, v_programid
from kpi_master kpim
where kpim.udf is not null
and kpim.isactive = 1
and exists ( select null
from vw_ticket tkt
where tkt.amsprogramid = v_programid)
) kpim
on (kpim.v_programid = pkpi.programid
and kpim.master_kpi_id = pkpi.master_kpi_id)
when not matched then
insert values (kpim.v_programid, kpim.master_kpi_id, sysdate)
when matched then
update
set pkpi.lastupdatedbydataload = sysdate;
Please check the results of this code with your expected outcome. As I said, reverse-engineering business logic is hard, and matching on master_kpi_id as well as programid is not the same as just deleting on programid.
You do not change v_UDF after first fetch. Then loop compare it with same first value... compare and compare... compare and compare.

Iterate over a column in PL/SQL

I have a table Emp with EmpID, Empname, Salary and I am trying to do a calculation for each employee. But I am having problems trying to iterate over each emp to do the calculation. I cant use explicit cursors though.
So right now I am just trying to create the list of empIDs:
Declare
aRows Number;
eid emp_ID%TYPE;
Begin
Select Count(*)
Into aRows
from emp;
Select emp_ID
Into eid
From emp;
FOR days IN 1..Tot_Rows
Loop
Dbms_Output.Put_Line(eid);
eid := eid + 1;
End Loop;
END;
But I get the error:
PLS-00320: the declaration of the type of this expression is incomplete or malformed
The simplest way to iterate over the rows in a table in PL/SQL is to do something like
BEGIN
FOR employees IN (SELECT emp_id FROM emp)
LOOP
dbms_output.put_line( employees.emp_id );
END LOOP;
END;
Alternately, you could fetch all the EID values into a PL/SQL collection and iterate over the collection, as in this example
DECLARE
TYPE emp_id_tbl IS TABLE OF emp.emp_id%type;
l_emp_ids emp_id_tbl ;
BEGIN
SELECT emp_id
BULK COLLECT INTO l_emp_ids
FROM emp;
FOR i IN l_emp_ids .FIRST .. l_empnos.LAST
LOOP
dbms_output.put_line( l_emp_ids (i) );
END LOOP;
END;
If your query can return thousands of rows, however, fetching all the data into the collection may use more of the PGA memory than you'd like and you may need to fetch rows in chunks using the LIMIT clause. But that would seem to be getting ahead of ourselves at this point.
Justin Cave has explained how to do it, but to specifically look at the error you got, that was because of this:
eid emp_ID%TYPE;
When using the %TYPE you have to specify the table name as well as the column name:
eid emp.emp_ID%TYPE;
If you were selecting all the columns in the row you could also look at %ROWTYPE.
Your approach was also making two assumptions: that the initial select into eid found the lowest ID, which is by no means guaranteed; and that all the subsequent ID values are sequential. And you're declaring and populating aRows but referring to Tot_Rows.

Resources