By Franck Pachot

.
A good thing from 12.2 is that the implementation of Unified Audit Trail has changed a little. It was stored in a weird CLI_SWP$ table but now we have a normal partitioned table named AUD$UNIFIED. In a previous post I traced the two purge method: purge all before a timestamp, or purge all. Here is the same in 12.2

Purge old

I have quite a few record in Unified Audit Trail here.


SQL> select unified_audit_policies,action_name,count(*) from unified_audit_trail group by unified_audit_policies,action_name;
 
UNIFIED_AUDIT_POLICIES                   ACTION_NAME            COUNT(*)
---------------------------------------- -------------------- ----------
                                         EXECUTE                       3
ORA_LOGON_FAILURES                       LOGON                    408275

I set the timestamp to 6 hours before now

SQL> exec dbms_audit_mgmt.set_last_archive_timestamp(audit_trail_type=>dbms_audit_mgmt.audit_trail_unified
,last_archive_time=>sysdate-6/24);
PL/SQL procedure successfully completed.

And call the clean procedure:

SQL> exec dbms_audit_mgmt.clean_audit_trail(audit_trail_type=>dbms_audit_mgmt.audit_trail_unified
,use_last_arch_timestamp=>TRUE);
PL/SQL procedure successfully completed.

Here is what I see in the trace:

select sys.dbms_audit_mgmt.is_droppable_partition(:1, :2)
from
 dual

This is interesting. The Unified Audit Trail is partitioned on timestamp and the purge procedure checks it the partition can be dropped instead of running a long delete statement.

Here is the documentation we have about it in ?/rdbms/admin/dbmsamgt.sql

  -- is_droppable_partition - IS aud$unified table PARTITION DROPPABLE?
  --
  --
  -- INPUT PARAMETERS
  -- partname - aud$unified table's Partition Name
  -- lat      - Last Archive Timestamp mentioned during cleanup

In my case, I traced the bind variables and the is_droppable_partition procedure was run only once with partname=>'”SYS_P348″‘ and lat=>’03-MAR-17 03.07.56 PM’. The timestamp is the ‘last timestamp’ I’ve set, and I have only one partition here because my database was created recently.

As we can guess, this checks the high value of the partition:

select high_value
from
 dba_tab_partitions where table_owner = 'AUDSYS' and table_name =
  'AUD$UNIFIED' and partition_name = :1

Because I have only one partition, which is the current one, my ‘last timestamp’ is below the high_value so it is not possible to truncate this partition and keep the records from after the ‘last timestamp’.

Then a delete is run, which deletes all rows from before my last timestamp (bind variable :1 is ’03-MAR-17 03.07.56 PM’). Note that I don’t know (yet) why we can have DBID=0.

delete from audsys.aud$unified
where
 event_timestamp < :1 and  (dbid = :2 or dbid = 0)
 
call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      2     10.68      31.25      16885      24367     437518      346517
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3     10.68      31.25      16885      24367     437518      346517
 
Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: ALL_ROWS
Parsing user id: SYS   (recursive depth: 1)
Number of plan statistics captured: 1
 
Rows (1st) Rows (avg) Rows (max)  Row Source Operation
---------- ---------- ----------  ---------------------------------------------------
         0          0          0  DELETE  AUD$UNIFIED (cr=12219 pr=16885 pw=0 time=31219023 us starts=1)
    346517     346517     346517   PARTITION RANGE ITERATOR PARTITION: 1 KEY (cr=12148 pr=0 pw=0 time=1161311 us starts=1 cost=547 size=1231218 card=68401)
    346517     346517     346517    TABLE ACCESS FULL AUD$UNIFIED PARTITION: 1 KEY (cr=12148 pr=0 pw=0 time=788043 us starts=1 cost=547 size=1231218 card=68401)
 
Elapsed times include waiting on following events:
  Event waited on                             Times   Max. Wait  Total Waited
  ----------------------------------------   Waited  ----------  ------------
  PGA memory operation                            5        0.00          0.00
  db file sequential read                     16885        0.03         21.03

All my rows are deleted with conventional updates here. I had 400000 rows, deleted 340000 so 60000 remains.

Purge old with old partitions

I had only one partition here but AUDSYS.AUD$UNIFIED is partitioned by month. Here is what I can see in my alert.log about the creation of this partition:

TABLE AUDSYS.AUD$UNIFIED: ADDED INTERVAL PARTITION SYS_P348 (33) VALUES LESS THAN (TIMESTAMP' 2017-04-01 00:00:00')

Actually, this is automatically partitioned by months. Here is an excerpt of the table’s DDL as displayed by dbms_metadata:

  PARTITION BY RANGE ("EVENT_TIMESTAMP") INTERVAL (INTERVAL '1' MONTH)
 (PARTITION "SYS_P348"  VALUES LESS THAN (TIMESTAMP' 2017-04-01 00:00:00') SEGMENT CREATION IMMEDIATE

When running the same as before but on a database with few older partitions (because there were no scheduled purge) I can see that the ‘is_droppable_partition’ and the related query is run 4 times:

select high_value
from
 dba_tab_partitions where table_owner = 'AUDSYS' and table_name =
  'AUD$UNIFIED' and partition_name = :1
 
call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      4      0.05       0.05          0          0          0           0
Fetch        4      0.00       0.00          0        143          0           4
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        9      0.05       0.06          0        143          0           4

and I see a drop partition for the 3 old partitions:

ALTER TABLE AUDSYS.AUD$UNIFIED DROP PARTITION AUD_UNIFIED_P0
ALTER TABLE AUDSYS.AUD$UNIFIED DROP PARTITION SYS_P221
ALTER TABLE AUDSYS.AUD$UNIFIED DROP PARTITION SYS_P781

Note that this is called by:

CALL DBMS_PDB_EXEC_SQL('ALTER TABLE AUDSYS.AUD$UNIFIED DROP PARTITION SYS_P781')

which runs it internally as an oracle script because this DDL is not allowed otherwise.

In summary, purging with a timestamp is optimized to run conventional deletes only on latest partition. Older partitions are dropped. If you schedule a job to regularly set the timestamp and then have the purge job doing the cleaning, then better to set a timestamp at the beginning of the month. If you have to purge a large audit trail, then better to wait the beginning of the next month.

Purge all

If you don’t need to keep recent records and want to truncate all, then just call the purge without timestamp.

Here I have about 60000 rows remaining from the previous test, all in the current partition.

SQL> select unified_audit_policies,action_name,count(*) from unified_audit_trail group by unified_audit_policies,action_name;
 
UNIFIED_AUDIT_POLICIES                   ACTION_NAME            COUNT(*)
---------------------------------------- -------------------- ----------
                                         EXECUTE                       6
ORA_LOGON_FAILURES                       LOGON                     62152

I call the clean

SQL> exec dbms_audit_mgmt.clean_audit_trail(audit_trail_type=>dbms_audit_mgmt.audit_trail_unified
,use_last_arch_timestamp=>FALSE);
PL/SQL procedure successfully completed.

And I can see directly in the trace a truncate of the whole table:

TRUNCATE TABLE AUDSYS.AUD$UNIFIED
 
call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.04       4.42         67          6        919           0
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        2      0.04       4.42         67          6        919           0
 
Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: SYS   (recursive depth: 1)
 
Elapsed times include waiting on following events:
  Event waited on                             Times   Max. Wait  Total Waited
  ----------------------------------------   Waited  ----------  ------------
  db file sequential read                        67        0.00          0.07
  enq: RO - fast object reuse                     5        0.94          1.76
  local write wait                               36        0.14          2.54

This is the fastest way to empty the Unified Audit Trail.

So what?

We don’t have long experience on 12.2 production yet, but from what I see here, this new implementation is a good thing. There were many problems with the 12.1 implementations that are probably solved by having a normal table with normal interval partitioning, purged with normal deletes and normal truncates.
Of course, the next question is what happens when you upgrade a 12.1 database with a huge audit trail? That’s for a future post.
And don’t forget that by default you are in mixed mode. More info here.