{
  "date": "2017-03-19T11:12:42",
  "slug": "purging-unified-audit-trail-in-12cr2",
  "link": "https://www.dbi-services.com/blog/purging-unified-audit-trail-in-12cr2/",
  "title": {
    "rendered": "Purging Unified Audit Trail in 12cR2"
  },
  "content": {
    "rendered": "<h2>By Franck Pachot</h2>\n<p>.<br />\nA 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 <a href=\"https://www.dbi-services.com/blog/purging-unified-audit-trail-in-12cr1/\" target=\"_blank\" rel=\"noopener noreferrer\">previous post</a> I traced the two purge method: purge all before a timestamp, or purge all. Here is the same in 12.2<br />\n<!--more--></p>\n<h3>Purge old</h3>\n<p>I have quite a few record in Unified Audit Trail here.</p>\n<pre><code>\nSQL&gt; select unified_audit_policies,action_name,count(*) from unified_audit_trail group by unified_audit_policies,action_name;\n&nbsp;\nUNIFIED_AUDIT_POLICIES                   ACTION_NAME            COUNT(*)\n---------------------------------------- -------------------- ----------\n                                         EXECUTE                       3\nORA_LOGON_FAILURES                       LOGON                    408275\n</code></pre>\n<p>I set the timestamp to 6 hours before now</p>\n<pre><code>SQL&gt; exec dbms_audit_mgmt.set_last_archive_timestamp(audit_trail_type=&gt;dbms_audit_mgmt.audit_trail_unified\n,last_archive_time=&gt;sysdate-6/24);\nPL/SQL procedure successfully completed.</code></pre>\n<p>And call the clean procedure:</p>\n<pre><code>SQL&gt; exec dbms_audit_mgmt.clean_audit_trail(audit_trail_type=&gt;dbms_audit_mgmt.audit_trail_unified\n,use_last_arch_timestamp=&gt;TRUE);\nPL/SQL procedure successfully completed.</code></pre>\n<p>Here is what I see in the trace:</p>\n<pre><code>select sys.dbms_audit_mgmt.is_droppable_partition(:1, :2)\nfrom\n dual</code></pre>\n<p>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.</p>\n<p>Here is the documentation we have about it in ?/rdbms/admin/dbmsamgt.sql</p>\n<pre><code>  -- is_droppable_partition - IS aud$unified table PARTITION DROPPABLE?\n  --\n  --\n  -- INPUT PARAMETERS\n  -- partname - aud$unified table's Partition Name\n  -- lat      - Last Archive Timestamp mentioned during cleanup\n</code></pre>\n<p>In my case, I traced the bind variables and the is_droppable_partition procedure was run only once with partname=&gt;'&#8221;SYS_P348&#8243;&#8216; and lat=&gt;&#8217;03-MAR-17 03.07.56 PM&#8217;. The timestamp is the &#8216;last timestamp&#8217; I&#8217;ve set, and I have only one partition here because my database was created recently.</p>\n<p>As we can guess, this checks the high value of the partition:</p>\n<pre><code>select high_value\nfrom\n dba_tab_partitions where table_owner = 'AUDSYS' and table_name =\n  'AUD$UNIFIED' and partition_name = :1</code></pre>\n<p>Because I have only one partition, which is the current one, my &#8216;last timestamp&#8217; is below the high_value so it is not possible to truncate this partition and keep the records from after the &#8216;last timestamp&#8217;.</p>\n<p>Then a delete is run, which deletes all rows from before my last timestamp (bind variable :1 is &#8217;03-MAR-17 03.07.56 PM&#8217;). Note that I don&#8217;t know (yet) why we can have DBID=0.</p>\n<pre><code>delete from audsys.aud$unified\nwhere\n event_timestamp &lt; :1 and  (dbid = :2 or dbid = 0)\n&nbsp;\ncall     count       cpu    elapsed       disk      query    current        rows\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\nParse        1      0.00       0.00          0          0          0           0\nExecute      2     10.68      31.25      16885      24367     437518      346517\nFetch        0      0.00       0.00          0          0          0           0\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\ntotal        3     10.68      31.25      16885      24367     437518      346517\n&nbsp;\nMisses in library cache during parse: 1\nMisses in library cache during execute: 1\nOptimizer mode: ALL_ROWS\nParsing user id: SYS   (recursive depth: 1)\nNumber of plan statistics captured: 1\n&nbsp;\nRows (1st) Rows (avg) Rows (max)  Row Source Operation\n---------- ---------- ----------  ---------------------------------------------------\n         0          0          0  DELETE  AUD$UNIFIED (cr=12219 pr=16885 pw=0 time=31219023 us starts=1)\n    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)\n    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)\n&nbsp;\nElapsed times include waiting on following events:\n  Event waited on                             Times   Max. Wait  Total Waited\n  ----------------------------------------   Waited  ----------  ------------\n  PGA memory operation                            5        0.00          0.00\n  db file sequential read                     16885        0.03         21.03\n</code></pre>\n<p>All my rows are deleted with conventional updates here. I had 400000 rows, deleted 340000 so 60000 remains.</p>\n<h3>Purge old with old partitions</h3>\n<p>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:</p>\n<pre><code>TABLE AUDSYS.AUD$UNIFIED: ADDED INTERVAL PARTITION SYS_P348 (33) VALUES LESS THAN (TIMESTAMP' 2017-04-01 00:00:00')</code></pre>\n<p>Actually, this is automatically partitioned by months. Here is an excerpt of the table&#8217;s DDL as displayed by dbms_metadata:</p>\n<pre><code>  PARTITION BY RANGE (\"EVENT_TIMESTAMP\") INTERVAL (INTERVAL '1' MONTH)\n (PARTITION \"SYS_P348\"  VALUES LESS THAN (TIMESTAMP' 2017-04-01 00:00:00') SEGMENT CREATION IMMEDIATE</code></pre>\n<p>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 &#8216;is_droppable_partition&#8217; and the related query is run 4 times:</p>\n<pre><code>select high_value\nfrom\n dba_tab_partitions where table_owner = 'AUDSYS' and table_name =\n  'AUD$UNIFIED' and partition_name = :1\n&nbsp;\ncall     count       cpu    elapsed       disk      query    current        rows\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\nParse        1      0.00       0.00          0          0          0           0\nExecute      4      0.05       0.05          0          0          0           0\nFetch        4      0.00       0.00          0        143          0           4\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\ntotal        9      0.05       0.06          0        143          0           4</code></pre>\n<p>and I see a drop partition for the 3 old partitions:</p>\n<pre><code>ALTER TABLE AUDSYS.AUD$UNIFIED DROP PARTITION AUD_UNIFIED_P0\nALTER TABLE AUDSYS.AUD$UNIFIED DROP PARTITION SYS_P221\nALTER TABLE AUDSYS.AUD$UNIFIED DROP PARTITION SYS_P781</code></pre>\n<p>Note that this is called by:</p>\n<pre><code>CALL DBMS_PDB_EXEC_SQL('ALTER TABLE AUDSYS.AUD$UNIFIED DROP PARTITION SYS_P781')</code></pre>\n<p>which runs it internally as an oracle script because this DDL is not allowed otherwise.</p>\n<p>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.</p>\n<h3>Purge all</h3>\n<p>If you don&#8217;t need to keep recent records and want to truncate all, then just call the purge without timestamp.</p>\n<p>Here I have about 60000 rows remaining from the previous test, all in the current partition.</p>\n<pre><code>SQL&gt; select unified_audit_policies,action_name,count(*) from unified_audit_trail group by unified_audit_policies,action_name;\n&nbsp;\nUNIFIED_AUDIT_POLICIES                   ACTION_NAME            COUNT(*)\n---------------------------------------- -------------------- ----------\n                                         EXECUTE                       6\nORA_LOGON_FAILURES                       LOGON                     62152\n</code></pre>\n<p>I call the clean</p>\n<pre><code>SQL&gt; exec dbms_audit_mgmt.clean_audit_trail(audit_trail_type=&gt;dbms_audit_mgmt.audit_trail_unified\n,use_last_arch_timestamp=&gt;FALSE);\nPL/SQL procedure successfully completed.</code></pre>\n<p>And I can see directly in the trace a truncate of the whole table:</p>\n<pre><code>TRUNCATE TABLE AUDSYS.AUD$UNIFIED\n&nbsp;\ncall     count       cpu    elapsed       disk      query    current        rows\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\nParse        1      0.00       0.00          0          0          0           0\nExecute      1      0.04       4.42         67          6        919           0\nFetch        0      0.00       0.00          0          0          0           0\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\ntotal        2      0.04       4.42         67          6        919           0\n&nbsp;\nMisses in library cache during parse: 1\nOptimizer mode: ALL_ROWS\nParsing user id: SYS   (recursive depth: 1)\n&nbsp;\nElapsed times include waiting on following events:\n  Event waited on                             Times   Max. Wait  Total Waited\n  ----------------------------------------   Waited  ----------  ------------\n  db file sequential read                        67        0.00          0.07\n  enq: RO - fast object reuse                     5        0.94          1.76\n  local write wait                               36        0.14          2.54</code></pre>\n<p>This is the fastest way to empty the Unified Audit Trail.</p>\n<h3>So what?</h3>\n<p>We don&#8217;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.<br />\nOf course, the next question is what happens when you upgrade a 12.1 database with a huge audit trail? That&#8217;s for a future post.<br />\nAnd don&#8217;t forget that by default you are in mixed mode. More info <a href=\"https://www.dbi-services.com/blog/12c-unified-auditing-and-audit_traildb-in-mixed-mode/\" target=\"_blank\" rel=\"noopener noreferrer\">here</a>.</p>\n",
    "protected": false
  }
}
