INCONSISTENCY UPDATING all salaries of a table - oracle

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.

Related

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;

What is the proper way to create an update trigger with a variable getting value from the same table?

I'm creating a update trigger where an employee can never have a salary that is greater than the president's. However I need to subquery the president's salary for comparison and the "new" updated employee's salary.
I originally had the the subquery using from the from employees table but had to make a new table because of the mutating table problem. I don't think creating a new table is plausible solution for a real implementation.
Is there a way I can save the president's salary without creating a new table?
CREATE OR REPLACE TRIGGER prevent_salary
BEFORE UPDATE ON employees
FOR EACH ROW
declare pres_sal number(8,2);
BEGIN
select salary into pres_sal from employees_salary where job_id='AD_PRES';--employees_salary was employees but that gives mutating error
IF (:new.salary > pres_sal)
THEN UPDATE employees
SET salary = :old.salary
WHERE employee_id = :old.employee_id;
END IF;
END;
One way to do it is to save off the president's salary in a BEFORE STATEMENT trigger and then use that in the FOR EACH ROW trigger.
"Compound Triggers", which have been around at least since version 11.1, offer a nice way to do that all in one place.
Here is an example:
CREATE OR REPLACE TRIGGER prevent_salary
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER
pres_sal NUMBER;
BEFORE STATEMENT IS
BEGIN
select salary
into pres_sal
from employees
where job_id='AD_PRES';
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
:new.salary := least(:new.salary, pres_sal);
END BEFORE EACH ROW;
END prevent_salary;
You may try this :
CREATE OR REPLACE TRIGGER prevent_salary
BEFORE UPDATE ON employees
FOR EACH ROW
declare pres_sal number(8,2);
BEGIN
select salary into pres_sal from employees_salary where job_id='AD_PRES';--employees_salary was employees but that gives mutating error
IF (:new.salary > pres_sal)
Raise_Application_Error (-20101, 'An employee''s salary couldn''t exceed president''s !');
END IF;
END;

database update on large table oracle taking huge time

I want to copy one column data into another column in a large table containing 10 millions records.
I am using sys refcursor to copy data from one column into another column. It will taking more than 30 min to copy the data. I am using ORACLE 11gR2.
Is there any others alternative to do the same. Below is the scripts
CREATE OR REPLACE PROCEDURE tblCursor(org_mig OUT SYS_REFCURSOR)
IS
BEGIN
OPEN org_mig FOR
select id from tbl;
END;
/
DECLARE
org_mig SYS_REFCURSOR;
t_id organization.id%TYPE;
loop_var number(10);
commit_interval number(10);
BEGIN
loop_var :=1;
commit_interval:=10000;
tblCursor(org_mig);
LOOP
FETCH org_mig INTO t_id;
EXIT WHEN org_mig%NOTFOUND;
update tbl set col1=col2 where id=t_id;
IF mod(loop_var,commit_interval)=0 THEN
Commit;
End if;
loop_var :=loop_var+1;
END LOOP;
Commit;
CLOSE org_mig;
END;
/
You're doing this for every row in tbl, right? If so, you should just do this:
update tbl
set col1 = col2
/
Updating ten million rows will take some time, but a set operation will be way faster than the Row By Agonizing Row approach you've implemented. Plus, batching up your commits like that is bad practice. Not only does it slow things down, that approach can lead to ORA-01555: Snapshot too old exceptions. Find out more.
Still it has been taken long time to update.
I am trying with different one but getting error.
-----------------------------------
Error starting at line : 43 in command -
SELECT *
FROM TABLE(test_parallel_update(CURSOR(SELECT * FROM organization)))
Error report -
SQL Error: ORA-12801: error signaled in parallel query server P003
ORA-00932: inconsistent datatypes: expected - got -
ORA-06512: at "QA249.TEST_PARALLEL_UPDATE", line 21
12801. 00000 - "error signaled in parallel query server %s"
*Cause: A parallel query server reached an exception condition.
*Action: Check the following error message for the cause, and consult
your error manual for the appropriate action.
*Comment: This error can be turned off with event 10397, in which
case the server's actual error is signaled instead.
---------------------------
Here is the script:
CREATE OR REPLACE TYPE test_num_arr AS TABLE OF NUMBER;
CREATE OR REPLACE FUNCTION test_parallel_update (
test_cur IN SYS_REFCURSOR
)
RETURN test_num_arr
PARALLEL_ENABLE (PARTITION test_cur BY ANY)
PIPELINED
IS
PRAGMA AUTONOMOUS_TRANSACTION;
test_rec organization%ROWTYPE;
TYPE num_tab_t IS TABLE OF NUMBER(10,0);
TYPE vc2_tab_t IS TABLE OF number(1,0);
id NUM_TAB_T;
org_type_old NUM_TAB_T;
IS_DELETED_old VC2_TAB_T;
cnt INTEGER := 0;
BEGIN
LOOP
FETCH test_cur BULK COLLECT INTO id, org_type_old, IS_DELETED_old LIMIT 1000;
EXIT WHEN id.COUNT() = 0;
FORALL i IN id.FIRST .. id.LAST
UPDATE organization
SET org_type = org_type_old(i)
, IS_DELETED = IS_DELETED_old(i)
WHERE id = id(i);
cnt := cnt + id.COUNT;
END LOOP;
CLOSE test_cur;
COMMIT;
PIPE ROW(cnt);
RETURN;
END;
/
show error;
---- To Execute ----
SELECT *
FROM TABLE(test_parallel_update(CURSOR(SELECT * FROM organization)));
Note:
Table
organization
(
id number(10,0),
org_type number(10,0),
org_type_old number(10,0),
IS_DELETED number(1,0),
IS_DELETED_OLD number(1,0)
);
where id is a primary key, Now I want copy org_type_old and IS_DELETED_OLD into org_type and IS_DELETED respectively.

How to assign value to oracle cursor while iterating

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;

PL/SQL: Executing Procedure

I have created a procedure. It is giving an error ( ORA-01422: exact fetch returns more than requested number of rows). As for a specific department_id there are more than one employees. But how to solve this problem ?
Create Procedure PP1
(ID in number, Percent in number, Sal out number, increase_sal out number) IS
Begin
Select salary, salary *(1+percent/100) into sal, increase_sal
From employees
where department_id= id;
DBMS_OUTPUT.PUT_LINE (sal || ' ' || increase_sal);
END;
/
Variable a number
Variable b number
Exec PP1 (100, 10, :a, :b)
Print a b
Thanks,
Kuntal Roy
My guess is that you want something like (untested)
CREATE TYPE num_tbl IS TABLE OF NUMBER;
CREATE PROCEDURE raise_dept_salaries( p_dept_id IN employees.department_id%type,
p_raise_pct IN NUMBER,
p_old_sals OUT num_tbl,
p_new_sals OUT num_tbl )
AS
BEGIN
SELECT salary
BULK COLLECT INTO p_old_sals
FROM employees
WHERE department_id = p_dept_id;
UPDATE employees
SET salary = salary * (1 + p_raise_pct)
WHERE department_id = p_dept_id
RETURNING salary
BULK COLLECT INTO p_new_sals;
END;
Now, splitting things up this way does introduce the possibility that some other session will modify the data between your first SELECT and your UPDATE so this isn't really safe to use in a multi-user environment. Of course, you really wouldn't want to return both the old and the new salaries in the first place since you already know that they are going to be directly related to each other. If you only returned the collection of new salaries, then you would only need a single UPDATE statement and you wouldn't have the race condition.

Resources