Audit operations with a trigger - sql

I have a trigger that fires in certain conditions, and when I update some data in EMPLOYEES table (specifically when inserting, deleting and updating comm_pct and salary) the changes that were made are registered into the following table:
CREATE TABLE "HR"."AUDIT_E" ("USR" VARCHAR2(30 BYTE) DEFAULT USER,
"DATE" DATE DEFAULT SYSDATE,
"DML_TYPE" VARCHAR2), -- UPDATE, INSERT, DELETE
"OLD_EMPLOYEE_ID" NUMBER,
"OLD_FIRST_NAME" VARCHAR2,
...,--more fields
"OLD_JOB_ID" VARCHAR2,
"OLD_SALARY" NUMBER,
"OLD_COMMISSION_PCT" NUMBER,
"NEW_FIRST_NAME" VARCHAR2,
..., -- more fields!
"NEW_JOB_ID" VARCHAR2,
"NEW_SALARY" NUMBER,
"NEW_COMMISSION_PCT" NUMBER)
My question is: How can I do an INSERT in AUDIT_E (Because I must register old and new values into it) when updating rows with another values (as email with comm_pct and other fields, besides only updating comm_pct and salary)? Because my trigger has the following structure:
IF DELETING THEN
--some actions
-- Insert into AUDIT_E(...) values...
ELSIF INSERTING THEN
--some actions
-- Insert into AUDIT_E(...) values...
ELSIF UPDATING ('a field') THEN --I have two of these
--some actions
-- Insert into AUDIT_E(...) values...comm_pct/salary
Thank you very much if you can help me and sorry for my english.
EDIT: My trigger runs fine registring changes into audit_e when I am inserting, deleting rows and updating only comm_pct and salary:
AUDIT_E:
ID |Oper|Old_Name|Old_job_id|Old_comm_pct|Old_Salary|New_name|New_job_id|New_comm_pct|New_salary
------------------------------------------------------------------------------------------------
1 |Ins | NULL | NULL | NULL | NULL |Kappa | SA_REP | 0.2 | 4980
2 |Upd | Kappa | SA_REP | 0.2 | 4980 | NULL | NULL | 0.3 | NULL
3 |Upd | Kappa | SA_REP | 0.3 | 4980 | NULL | NULL | NULL | 5000
4 |Del | Kappa | SA_REP | 0.3 | 4980 | NULL | NULL | NULL | NULL
But when I am changing the job_id for example (putting an additional elsif update), the changes are saved into audit_e wrong:
AUDIT_E:
ID |Oper|Old_Name|Old_job_id|Old_comm_pct|Old_Salary|New_name|New_job_id|New_comm_pct|New_salary
------------------------------------------------------------------------------------------------
1 |Upd |Kappa | SA_REP | 0.2 | 4980 | NULL | NULL | NULL | NULL
And I want those changes saved into audit_e table like this:
AUDIT_E:
ID |Oper|Old_Name|Old_job_id|Old_comm_pct|Old_Salary|New_name|New_job_id|New_comm_pct|New_salary
------------------------------------------------------------------------------------------------
1 |Upd |Kappa | SA_REP | 0.2 | 4980 | NULL | IT_PROG | NULL | NULL

I'm still not sure that I understand the question. My guess is that you just want something like
CREATE OR REPLACE TRIGGER trigger_name
AFTER INSERT OR UPDATE OR DELETE
ON employees
FOR EACH ROW
BEGIN
INSERT INTO audit_e( dml_type,
old_employee_id, new_employee_id,
old_first_name, new_first_name,
...
)
VALUES( CASE WHEN deleting
THEN 'D'
WHEN inserting
THEN 'I'
WHEN updating
THEN 'U'
ELSE 'X'
END,
:old.employee_id, :new.employee_id,
:old.first_name, :new.first_name,
...
);
END;
That said, it's not clear to me why you would bother storing the old and new data in your audit table each time. The old_* values are always going to be identical to the new_ values from the prior row. It generally makes sense to just store the new_ values in the audit table.

Related

Running multiple inserts into an Oracle table, how can I commit after each insert and restart the stored procedure at the last inserted point?

I'm very new to Oracle and am writing my first stored procedure for a side project. Essentially I have one table for intraday data, and another table to store historical data. I need to insert chunks of the intraday table into the history table, commit those inserts, and restart the stored procedure at the first uninserted point in the case of failure.
Here is what I have so far:
CREATE OR REPLACE PROCEDURE test_proc (p_array_size IN PLS_INTEGER DEFAULT 5000)
IS
TYPE ARRAY IS TABLE OF z_intraday%ROWTYPE;
l_data ARRAY;
CURSOR c IS SELECT *
FROM "intraday";
BEGIN
OPEN c;
LOOP
FETCH c BULK COLLECT INTO l_data LIMIT p_array_size;
FORALL i IN 1..l_data.COUNT
INSERT INTO history
VALUES l_data(i);
EXIT WHEN c%notfound
END LOOP;
COMMIT;
EXCEPTION WHEN OTHERS THEN
ROLLBACK;
RAISE;
CLOSE c;
END test_proc;
So I only commit after the loop has finished. How can I refactor so that each insert operation in the loop commits, then if there is a failure, roll back to the previous batch of records that failed and run the procedure again? Sorry I know this is a heavy question, but any guidance would be greatly appreciated.
Use set-based operations wherever possible, not row-by-row operations. A single "insert as select" or "merge" statement with filter would run faster by several orders of magnitude than the row-by-slow construct you have created. Also, committing after every individual row will kill your performance for the entire database instance, not just this procedure, as it forces checkpoints in the redo logs.
insert into history (col1, col2, col3)
as select col1, col2, col3 from intraday d
where d.id not in (select id from history);
commit;
or
merge into history h
using intraday d
on (h.id = d.id)
when not matched then
insert (h.id, h.col2, h.col3) values (d.id, d.col2, d.col3);
commit;
You don't need a complicated procedure, you can use INSERT INTO ... LOG ERRORS INTO ... to capture the errors and then all the errors can be put into one table and the valid rows will all be successfully inserted (continuing on from each error, if you specify REJECT LIMIT UNLIMITED).
If you have the tables:
CREATE TABLE "intraday" (
a INT PRIMARY KEY,
b DATE,
c TIMESTAMP,
d VARCHAR2(30),
e VARCHAR2(10)
);
CREATE TABLE history (
a INT,
b DATE,
c TIMESTAMP NOT NULL,
d VARCHAR2(30),
e DATE
);
INSERT INTO "intraday"
SELECT 1, DATE '2020-01-01', TIMESTAMP '2020-01-01 00:00:00', 'valid', '2020-01-01' FROM DUAL UNION ALL
SELECT 2, DATE '2020-01-02', NULL, 'timestamp null', '2020-01-01' FROM DUAL UNION ALL
SELECT 3, DATE '2020-01-03', TIMESTAMP '2020-01-03 00:00:00', 'implicit date cast fails', '2020-01-XX' FROM DUAL UNION ALL
SELECT 4, DATE '2020-01-04', TIMESTAMP '2020-01-04 00:00:00', 'valid', '2020-01-04' FROM DUAL;
ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD';
Then you can create a table to put the errors using:
BEGIN
DBMS_ERRLOG.CREATE_ERROR_LOG (
dml_table_name => 'HISTORY',
err_log_table_name => 'HISTORY_ERRORS'
);
END;
/
Then you can run the SQL statement:
INSERT /*+ APPEND */ INTO history
SELECT * FROM "intraday"
LOG ERRORS INTO history_errors ('INSERT APPEND') REJECT LIMIT UNLIMITED;
Then the history table will contain:
SELECT * FROM history;
A | B | C | D | E
-: | :--------- | :------------------------ | :---- | :---------
1 | 2020-01-01 | 01-JAN-20 00.00.00.000000 | valid | 2020-01-01
4 | 2020-01-04 | 04-JAN-20 00.00.00.000000 | valid | 2020-01-04
And the errors will be:
SELECT * FROM history_errors;
ORA_ERR_NUMBER$ | ORA_ERR_MESG$ | ORA_ERR_ROWID$ | ORA_ERR_OPTYP$ | ORA_ERR_TAG$ | A | B | C | D | E
--------------: | :----------------------------------------------------------------------------------- | :------------- | :------------- | :------------ | :- | :--------- | :--------------------------- | :----------------------- | :---------
1400 | ORA-01400: cannot insert NULL into ("FIDDLE_HSUKHKSUNFGTKKAMLHOA"."HISTORY"."C")<br> | null | I | INSERT APPEND | 2 | 2020-01-02 | null | timestamp null | 2020-01-01
1858 | ORA-01858: a non-numeric character was found where a numeric was expected<br> | null | I | INSERT APPEND | 3 | 2020-01-03 | 03-JAN-20 00.00.00.000000000 | implicit date cast fails | 2020-01-XX
db<>fiddle here

How to Lookup Values from Another Column and check if its a duplicate and then delete row in Oracle SQL

I am looking for some help to try and delete a number of rows from my data set that are duplicate rows. I currently have around 50k rows of data that identify faults that have been reported by customers and I have put these in time order and would like a single row for the sequence of faults. The following is an example of the output I get, unfortunately there isn't a column for the customer otherwise this would have made it easier.
| Fault | Date | Fault_2 | Date_2 | Fault_3 | Date_3 |
| 123 | 01-02-20 | 456 | 03-02-20 | 789 | 06-02-20 |
| 456 | 03-02-20 | 789 | 06-02-20 |
| 789 | 06-02-20 |
What I would like is to write a query that will look at column 'Fault_2' and try and find all the values from that column in the column 'Fault' and if any matches found then this will remove the whole row. This will be the same for the column 'Fault 3'. So the final output I will be left with will be the following:
| Fault | Date | Fault_2 | Date_2 | Fault_3 | Date_3 |
| 123 | 01-02-20 | 456 | 03-02-20 | 789 | 06-02-20 |
Would really appreciate if somebody could advise on how I go about achieving this.
My Create Table Script:
CREATE TABLE "Faults_Table"
( "FAULT" VARCHAR2(255 BYTE),
"DATE_" DATE,
"FAULT_2" VARCHAR2(255 BYTE),
"DATE_2" DATE,
"FAULT_3" VARCHAR2(255 BYTE),
"DATE_3" DATE
)
Insert Statements:
Insert into Faults_Table (FAULT,DATE_,FAULT_2,DATE_2,FAULT_3,DATE_3) values ('123',to_date('01-FEB-20 10:23:03','DD-MON-RR HH24:MI:SS'),'456',to_date('03-FEB-20 10:23:19','DD-MON-RR HH24:MI:SS'),'789',to_date('06-FEB-20 10:23:29','DD-MON-RR HH24:MI:SS'));
Insert into Faults_Table (FAULT,DATE_,FAULT_2,DATE_2,FAULT_3,DATE_3) values ('456',to_date('03-FEB-20 10:23:19','DD-MON-RR HH24:MI:SS'),'789',to_date('06-FEB-20 10:23:29','DD-MON-RR HH24:MI:SS'),null,null);
Insert into Faults_Table (FAULT,DATE_,FAULT_2,DATE_2,FAULT_3,DATE_3) values ('789',to_date('06-FEB-20 10:23:29','DD-MON-RR HH24:MI:SS'),null,null,null,null);
Thanks
This delete should do the job. It compares faults and dates:
delete
from t a where exists (
select 1 from t b
where (a.fault, a.date_) in ((fault_2, date_2)))
dbfiddle demo
This selects only the rows where Fault columns are not found in Fault_2 or Fault_3
SELECT
t1.*
FROM
table_name t1
WHERE t1.Fault NOT IN (SELECT Fault_2 FROM table_name )
AND t1.Fault NOT IN (SELECT Fault_3 FROM table_name )
I have managed to write the following query, which works.
SELECT
t1.*
FROM
faults_table t1
WHERE NOT EXISTS (SELECT *
FROM faults_table T2
WHERE T1.FAULT = T2.FAULT_2)
;

I can not use the 'insert all' to insert values into the table [duplicate]

This question already has answers here:
Multiple insert SQL oracle
(2 answers)
Closed 3 years ago.
I can not use the insert all to insert values into the first_name,last_name and phone columns.
CREATE TABLE accounts (
account_id NUMBER GENERATED BY DEFAULT AS IDENTITY,
first_name VARCHAR2(25) NOT NULL,
last_name VARCHAR2(25) NOT NULL,
email VARCHAR2(100),
phone VARCHAR2(12) ,
full_name VARCHAR2(51) GENERATED ALWAYS AS(
first_name || ' ' || last_name
),
PRIMARY KEY(account_id)
);
INSERT ALL
INTO accounts(first_name,last_name,phone)VALUES('John','Mobsey','410-555-0197')
INTO accounts(first_name,last_name,phone)VALUES('Ted','Scherbats','410-555-0198')
INTO accounts(first_name,last_name,phone)VALUES('Leeanna','Bowman','410-555-0199')
SELECT* FROM DUAL;
This is the error message I get when I try to Run the code:
ORA-00001: unique constraint (BTMDATABASE.SYS_C0086595925) violated
ORA-06512: at "SYS.DBMS_SQL", line 1721
1. INSERT ALL
2. INTO accounts(first_name,last_name,phone)VALUES('Trinity','Knox','410-555-0197')
3. INTO accounts(first_name,last_name,phone)VALUES('Mellissa','Porter','410-555-0198')
Exactly, you can not. The way you decided to create unique values for the account_id column won't work in insert all as all rows get the same value which violates the primary key constraint.
Two workarounds:
don't use insert all but separate insert statements
switch to a sequence in order to set primary key column's values
And, here's an example (if you need it):
SQL> create table accounts
2 (account_id number primary key,
3 first_name varchar2(20) not null
4 );
Table created.
SQL> create sequence seq_acc;
Sequence created.
SQL> create or replace trigger trg_acc_seq
2 before insert on accounts
3 for each row
4 begin
5 :new.account_id := seq_acc.nextval;
6 end;
7 /
Trigger created.
SQL> insert all
2 into accounts (first_name) values ('John')
3 into accounts (first_name) values ('Ted')
4 into accounts (first_name) values ('Leeanna')
5 select * from dual;
3 rows created.
SQL> select * from accounts;
ACCOUNT_ID FIRST_NAME
---------- --------------------
1 John
2 Ted
3 Leeanna
SQL>
Your statement:
INSERT ALL
INTO accounts(first_name,last_name,phone)VALUES('John','Mobsey','410-555-0197')
INTO accounts(first_name,last_name,phone)VALUES('Ted','Scherbats','410-555-0198')
INTO accounts(first_name,last_name,phone)VALUES('Leeanna','Bowman','410-555-0199')
SELECT* FROM DUAL;
Will try to give all the rows the same account_id which will violate your primary key.
Instead, you can use separate INSERT statements; or, if you want a single statement/transaction then you can wrap the INSERT statements in an anonymous PL/SQL block:
BEGIN
INSERT INTO accounts(first_name,last_name,phone)VALUES('John','Mobsey','410-555-0197');
INSERT INTO accounts(first_name,last_name,phone)VALUES('Ted','Scherbats','410-555-0198');
INSERT INTO accounts(first_name,last_name,phone)VALUES('Leeanna','Bowman','410-555-0199');
END;
/
or, you can also use INSERT INTO ... SELECT ... UNION ALL ...:
INSERT INTO accounts(first_name,last_name,phone)
SELECT 'Trinity', 'Knox', '410-555-0197' FROM DUAL UNION ALL
SELECT 'Mellissa','Porter','410-555-0198' FROM DUAL;
Output:
SELECT * FROM accounts;
ACCOUNT_ID | FIRST_NAME | LAST_NAME | EMAIL | PHONE | FULL_NAME
---------: | :--------- | :-------- | :---- | :----------- | :--------------
2 | John | Mobsey | null | 410-555-0197 | John Mobsey
3 | Ted | Scherbats | null | 410-555-0198 | Ted Scherbats
4 | Leeanna | Bowman | null | 410-555-0199 | Leeanna Bowman
5 | Trinity | Knox | null | 410-555-0197 | Trinity Knox
6 | Mellissa | Porter | null | 410-555-0198 | Mellissa Porter
Note: account_id of 1 is the failed INSERT ALL.
db<>fiddle here
INSERT INTO accounts(first_name,last_name,phone)VALUES('John','Mobsey','410-555-0197');
INSERT INTO accounts(first_name,last_name,phone)VALUES('Ted','Scherbats','410-555-0198');
INSERT INTO accounts(first_name,last_name,phone)VALUES('Leeanna','Bowman','410-555-0199');

Insert NULL if value matches the value of another column

Lets say I have a table : "MyTable" and I have two columns in it : "val" and "val_new".
Now I want to insert new value into "val_new" but if the values are equal('val' and 'val_new') I want to insert NULL instead.
----------------------
| id | val | val_new |
----------------------
| 1 | 5 | NULL |
----------------------
| 2 | 6 | NULL |
----------------------
Lets have this table for example.
Now :
UPDATE myTable mt
SET mt.val_new = '5'
WHERE mt.id = '1';
I want the value of val_new to remain NULL or be updated to NULL instead of '5'.
EDIT:
I want to UPDATE existing values not INSERTING new rows.
Your question seems a bit confusing in some parts. The way your word it seems like a new row is to be created but it also seems like you want to update it? Hopefully this is what you wanted :)
UPDATE testVal Set val_new = CASE WHEN val = 5 THEN NULL ELSE 5 END
testVal is your table. If you wanted to use a different number just replace both 5 with the number of your choice. I used 5 because you used it in your example.
You need not have to insert a record rather you will have to update the existing one. If you run your insert command a new record will be created. So you table will have
----------------
|val | val_new |
----------------
| 5 | NULL |
----------------
| 6 | NULL |
----------------
| | | <-- if val = val_new
----------------
| | 6 | <==if val<> val_new.
I guess you dont need this output. So the best option is to update the columns.
You can use case statement,
update <yourtable>
set val_new =case
when val_new= val then
NULL
else val_new
end

Is it possible to update an "order" column from within a trigger in MySQL?

We have a table in our system that would benefit from a numeric column so we can easily grab the 1st, 2nd, 3rd records for a job. We could, of course, update this column from the application itself, but I was hoping to do it in the database.
The final method must handle cases where users insert data that belongs in the "middle" of the results, as they may receive information out of order. They may also edit or delete records, so there will be corresponding update and delete triggers.
The table:
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`seq` int(11) unsigned NOT NULL,
`job_no` varchar(20) NOT NULL,
`date` date NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=7 DEFAULT CHARSET=latin1
And some example data:
mysql> SELECT * FROM test ORDER BY job_no, seq;
+----+-----+--------+------------+
| id | seq | job_no | date |
+----+-----+--------+------------+
| 5 | 1 | 123 | 2009-10-05 |
| 6 | 2 | 123 | 2009-10-01 |
| 4 | 1 | 123456 | 2009-11-02 |
| 3 | 2 | 123456 | 2009-11-10 |
| 2 | 3 | 123456 | 2009-11-19 |
+----+-----+--------+------------+
I was hoping to update the "seq" column from a t rigger, but this isn't allowed by MySQL, with an error "Can't update table 'test' in stored function/trigger because it is already used by statement which invoked this stored function/trigger".
My test trigger is as follows:
CREATE TRIGGER `test_after_ins_tr` AFTER INSERT ON `test`
FOR EACH ROW
BEGIN
SET #seq = 0;
UPDATE
`test` t
SET
t.`seq` = #seq := (SELECT #seq + 1)
WHERE
t.`job_no` = NEW.`job_no`
ORDER BY
t.`date`;
END;
Is there any way to achieve what I'm after other than remembering to call a function after each update to this table?
What about this?
CREATE TRIGGER `test_after_ins_tr` BEFORE INSERT ON `test`
FOR EACH ROW
BEGIN
SET #seq = (SELECT COALESCE(MAX(seq),0) + 1 FROM test t WHERE t.job_no = NEW.job_no);
SET NEW.seq = #seq;
END;
From Sergi's comment above:
http://dev.mysql.com/doc/refman/5.1/en/stored-program-restrictions.html - "Within a stored function or trigger, it is not permitted to modify a table that is already being used (for reading or writing) by the statement that invoked the function or trigger."