SQL trigger to stop update when a condition is met - oracle

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.

Related

How can I speed up the oracle query? ( where in( bla bla ))

I have 2000 numbers (Uniq Primary Key). I want to get contact information of numbers. My database is Oracle.
I use IN(bla,bla) in my query. It works slow because of this.
Example My Query:
SELECT p.*,t.*
FROM PERSONEL p
LEFT OUTER JOIN CODE_TITLE t ON t.Id = p.TitleId
WHERE ID IN(1,2,....,2000)
When the query runs, it takes about 10-12 seconds.
Is there a method to use instead of IN(bla, bla)? Can you explain with an example ?
Put your numbers (or whatever they really are) in a table. Let's call it LIST_TABLE. Then
SELECT p.*,t.*
FROM PERSONEL p
LEFT OUTER JOIN CODE_TITLE t ON t.Id = p.TitleId
WHERE ID IN(select list_id from list_table)
The type of table for LIST_TABLE (normal, GTT, external) will depend on where the values come from and your best mechanism for loading them.
You can use xmltable('1 to 2000') as a derived table in order to generate integer set starting form 1 upto 2000, incrementing by 1 :
SELECT p.*,t.*
FROM PERSONEL p
LEFT JOIN CODE_TITLE t ON t.Id = p.TitleId
WHERE Id IN ( SELECT TO_NUMBER(column_value) FROM xmltable('1 to 2000') )
P.S. indeed using WHERE ID BETWEEN 1 AND 2000 would suffice with index created on CODE_TITLE.ID in order to increase the query performance.
create or replace type myTcType as table of number(16,0);
create or replace function in_list(p_string in varchar2) return myTcType
as
l_data myTcType := myTcType();
l_string long default p_string || ',';
l_n number;
begin
loop
exit when l_string is null;
l_data.extend;
l_n := instr(l_string, ',');
l_data(l_data.count) := substr(l_string,1,l_n-1);
l_string := substr(l_string,l_n+1);
end loop;
return l_data;
end;
select * from table(cast(in_list('1,2,3,4,5') as myTcType));

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;

How to select max value from a temporary table whose field is unnamed

I'm writing a PL/SQL function that needs to get dates from a few different tables and return the most recent one. My approach has been this:
Create a temporary table to hold the dates:
CREATE TYPE t_dates IS TABLE OF DATE;
/
Create a few local variables:
l_date DATE;
l_dates t_dates := t_dates();
l_idx INTEGER := 0;
l_output_date DATE;
Then select into a variable each date I'm interested in, and add it to the temporary table:
SELECT it.date
INTO l_date
FROM interesting_table it
WHERE it.id = 1
;
l_dates.extend;
l_idx := l_idx + 1;
l_dates(l_idx) := l_date;
After the temporary table has been populated, now I just want to select the max value from it. How do I do that? Something like...
SELECT max(*)
INTO l_output_date
FROM l_dates
;
RETURN l_output_date;
But I'm not sure I can reference my temporary table like that, nor how to find the max of a column that is unnamed.
Edit: When I test the above, I get an error: PL/SQL: ORA-00936: missing expression related to the line where I have SELECT max(*). However, I don't believe that this is my only error, because if I change the last block to:
SELECT *
INTO l_output_date
FROM l_dates
WHERE rownum = 1
;
which, just for testing, should select the first date in the temp table, then the error becomes PL/SQL: ORA-00942: table or view does not exist, which indicates to me that I can't refer to my temp table in this manner.
I have since learned from the comments that I should have max(column_value) rather than max(*), but using this, I still get the table or view does not exist error.
It is not possible to select from the nested tables, like you have tried.
You may have to use a logic to find the maximum value
l_idx := l_idx + 1;
l_dates(l_idx) := l_date;
if(l_date > l_dates(l_idx-1) OR l_idx =1) THEN
max_date :=l_date;
End if;
For your case, Rather than fetching a list of dates then getting the max date, you can find the max date in your query itself
SELECT max(it.date)
INTO l_date
FROM interesting_table it
WHERE it.id = 1
;

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.

Oracle: Use a cursor to check whether a record in one table exists in another

As a POC for my non-technical team I need to come up with several ways to do the same thing which is to check whether a record in one table exists in another in order to see which is the most efficient. I've come up with two other ways that I am positive will be more efficient than a cursor, but I still need to show the time it takes to do this in a cursor. I can't figure out the syntax however.
I have two tables:
Table 1 has two fields I need and that I am fetching into the variables in the cursor:
Field = ID
Field = Account number
Table 2 has one field I need:
Field = Account Number (No ID available)
I need the ID from table 1 and the count of transactions where the account number from table 1 is not in table 2. Any suggestions?
Given that sample data:
SQL> SELECT * FROM T1;
ID
--
1
2
3
SQL> SELECT * FROM T2;
ID
--
2
3
4
The proper way to perform an anti-join T1 ▷ T2 is by writing:
SQL> SELECT ID FROM T1 WHERE ID NOT IN (SELECT ID FROM T2);
ID
--
1
Now, if you want to do it using cursor to show how slow that can be, you might want to use two nested loops like in the following example:
DECLARE
CURSOR c1 IS (SELECT ID FROM T1);
r1 c1%ROWTYPE;
CURSOR c2 IS (SELECT ID FROM T2);
r2 c2%ROWTYPE;
BEGIN
FOR r1 IN c1
LOOP
FOR r2 IN c2
LOOP
IF (r1.ID = r2.ID)
THEN
-- continue to the next iteration of the outer loop
-- as we have a match
GOTO continue;
END IF;
END LOOP;
-- we can only reach that point if there was no match
DBMS_OUTPUT.PUT_LINE(TO_CHAR(r1.ID));
<<continue>>
NULL;
END LOOP;
END;
I'm not sure if I follow you, but a simple cursor might look like this:
DECLARE
v_id NUMBER;
v_acct NUMBER;
BEGIN
FOR r1 IN ( SELECT ID, ACCT_NBR
FROM table1 T1
WHERE NOT EXISTS ( SELECT 1 FROM table2
where ID = T1.ID )
) LOOP
v_id := r1.id;
v_acct := r1.acct_nbr;
-- do something
END LOOP;
end;

Resources