With ever-changing regulatory and compliance requirements, auditing is becoming more important than ever. In this article, we will look at a common-method of enabling auditing within Oracle along with its benefits and drawbacks. In the next article, we will look at a new Oracle solution for auditing.
Let’s create a simple table with some data that will need auditing:
CREATE TABLE test AS
SELECT object_id, object_name, status, created
FROM all_objects;
A common method of enabling auditing is to create a historical table with all of the same columns as the table to be audited (along with a few historical columns). For our purposes:
CREATE TABLE test_hist AS
SELECT object_id, object_name, status, created,
CAST (NULL as VARCHAR2(1)) oper,
CAST (NULL as TIMESTAMP) modified_date
FROM test
WHERE 1 = 0;
Then we need a trigger to populate the historical table whenever the main table is changed. In this case, we are just going to audit any updates, but similar triggers can and should be added for inserts and deletes.
CREATE OR REPLACE TRIGGER audit_test
AFTER UPDATE
ON TEST
FOR EACH ROW
DECLARE
/**********************************************************************
NAME: Test
PURPOSE:
REVISIONS:
Ver Date Author Description
--------- ---------- --------------- ----------------------------
1.0 5/16/2012 dbolton 1. Created this trigger.
NOTES:
*********************************************************************/
BEGIN
IF ( (:NEW.object_id <> :OLD.object_id)
OR (:NEW.object_name <> :OLD.object_name)
OR (:NEW.status <> :OLD.status)
OR (:NEW.created <> :OLD.created))
THEN
INSERT INTO test_hist
(object_id,
object_name,
status,
created,
oper,
modified_date)
VALUES (:OLD.object_id,
:OLD.object_name,
:OLD.status,
:OLD.created,
'U',
SYSTIMESTAMP);
END IF;
END audit_test;
/
With that, we have enabled simple auditing on our main table. As a quick test:
SQL> SELECT COUNT (*)
2 FROM test_hist;
COUNT(*)
----------
0
SQL> UPDATE test
2 SET object_name = SUBSTR( object_name, 1, 29) || '-'
3 WHERE MOD(object_id, 10) = 0;
5573 rows updated.
SQL> SELECT COUNT (*)
2 FROM test_hist;
COUNT(*)
----------
5573
But let’s take a quick look behind the scenes at what is actually happening. If we turn on autotrace:
SQL> SET AUTOTRACE ON
SQL> UPDATE test
2 SET object_name = SUBSTR( object_name, 1, 29) || '-'
3 WHERE MOD(object_id, 10) = 0;
5573 rows updated.
Elapsed: 00:00:00.22
Execution Plan
----------------------------------------------------------
Plan hash value: 839355234
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | UPDATE STATEMENT | | 4997 | 214K| 111 (1)| 00:00:02 |
| 1 | UPDATE | TEST | | | | |
|* 2 | TABLE ACCESS FULL| TEST | 4997 | 214K| 111 (1)| 00:00:02 |
---------------------------------------------------------------------------
Statistics
----------------------------------------------------------
6365 recursive calls
12121 db block gets
689 consistent gets
0 physical reads
3467532 redo size
838 bytes sent via SQL*Net to client
853 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
3 sorts (memory)
0 sorts (disk)
5573 rows processed
SQL> ROLLBACK;
Rollback complete.
SQL> ALTER TRIGGER audit_test DISABLE;
Trigger altered.
SQL> UPDATE test
2 SET object_name = SUBSTR( object_name, 1, 29) || '-'
3 WHERE MOD(object_id, 10) = 0;
5573 rows updated.
Elapsed: 00:00:00.07
Execution Plan
----------------------------------------------------------
Plan hash value: 839355234
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | UPDATE STATEMENT | | 4997 | 146K| 111 (1)| 00:00:02 |
| 1 | UPDATE | TEST | | | | |
|* 2 | TABLE ACCESS FULL| TEST | 4997 | 146K| 111 (1)| 00:00:02 |
---------------------------------------------------------------------------
Statistics
----------------------------------------------------------
7 recursive calls
4232 db block gets
453 consistent gets
0 physical reads
1603712 redo size
832 bytes sent via SQL*Net to client
853 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
5573 rows processed
So the explain plan is exactly the same, but there are a lot more recursive calls and block gets with the trigger enabled. Looking at a tracefile, we see that is the result of the trigger operation:
*********************************************************************
UPDATE test
SET object_name = SUBSTR( object_name, 1, 29) || '-'
WHERE MOD(object_id, 10) = 0
call count cpu elapsed disk query rows
------- ------ -------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.02 0 1 0
Execute 1 0.38 0.39 0 389 5573
Fetch 0 0.00 0.00 0 0 0
------- ------ -------- ---------- ---------- ---------- ----------
total 2 0.38 0.41 0 390 5573
*********************************************************************
INSERT INTO TEST_HIST
VALUES
(:B1 , :B2 , :B3 , :B4 , 'U', SYSTIMESTAMP)
call count cpu elapsed disk query rows
------- ------ -------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0
Execute 5573 0.10 0.10 0 52 5573
Fetch 0 0.00 0.00 0 0 0
------- ------ -------- ---------- ---------- ---------- ----------
total 5574 0.10 0.10 0 52 5573
*********************************************************************
So the trigger has a good deal of overhead involved when using this auditing solution. Additionally, this method relies on the trigger being enabled. If that trigger is disabled, then the auditing does not take place. If the table is changed, the historical table and the trigger must be updated as well.
Finally, this method does not make retrieval of the audit information simple. In this example, we write the old record to the historical table. So any unmodified records are only in the main table. A UNION is needed to see the original state of the table. We could change this to write all the original records to the historical table and then only write the new values to the table. However, this requires storing duplicate data in the historical and main tables, which is not an ideal situation.
In the next article, we will look at Oracle Total Recall, which is an integrated audit solution release will Oracle Database 11g.
Link to script that contains the examples in this post.