Monitoring Table Change in Oracle

Database replication is no longer restricted to Oracle-to-Oracle configurations; Oracle-to-cloud and Oracle-to-BigQuery are just two of the various options that can now be selected for replication configurations. In a good number of these configurations lies GoldenGate as the tool of choice, given its versatility and reliability. Unfortunately, when replicating Oracle to another platform, actions such as table modifications can throw a monkey-wrench into the works. Thus, it would be desirable to track such changes in anticipation of handling GoldenGate extract abends gracefully and quickly. Let’s look at the possible scenarios and determine the best course of action.

The first thought the DBA might have is Unified Auditing, as it provides a wealth of information for auditable actions. Alas ‘audit table’ is not among the list of available privileges to audit:


SCOTT @ orcl > create audit policy alter_tab_pol
  2  privileges alter table;
privileges alter table
           *
ERROR at line 2:
ORA-46355: missing or invalid privilege audit option.


SCOTT @ orcl >

Interestingly the ‘ALTER ANY TABLE’ privilege is auditable, but it doesn’t audit what you might think would be audited:


SCOTT @ orcl > create audit policy table_pol
  2  privileges create any table, alter any table, drop any table;

Audit policy created.

SCOTT @ orcl > audit policy table_pol;

Audit succeeded.

SCOTT @ orcl > 

Such a policy only audits the granting of such privileges to other users and might not always produce an audit record. The requirement is yet unfulfilled by auditing so another solution must be produced. Fortunately, Oracle offers system-level triggers which can produce audit records for such actions. An example of how this might be done is shown below. First a table is created to contain the audit records generated:


create table ddl_log (
operation   varchar2(30),
obj_owner   varchar2(35),
object_name varchar2(35),
sql_text    varchar2(200),
attempt_by  varchar2(35),
attempt_dt  timestamp);
 
create index ddl_log_idx 
on ddl_log(obj_owner, operation);

The table is indexed on obj_owner and operation to speed up report generation. Next a trigger is created as the user who owns the tables to be monitored to log all CREATE, ALTER and DROP statements that have been executed:


create or replace trigger ddl_trigger
before create or alter or drop
on schema

declare
 oper ddl_log.operation%type;
 sql_text ora_name_list_t;
 i        pls_integer; 
begin
  i := sql_txt(sql_text);
  if i = 1 then
        insert into ddl_log
        select ora_sysevent, ora_dict_obj_owner,
        ora_dict_obj_name, sql_text(1), user, v_systimestamp
        from dual;
  elsif i = 2 then
        insert into ddl_log
        select ora_sysevent, ora_dict_obj_owner,
        ora_dict_obj_name, sql_text(1)||sql_text(2), user, v_systimestamp
        from dual;
  elsif i >= 3 then
        insert into ddl_log
        select ora_sysevent, ora_dict_obj_owner,
        ora_dict_obj_name, sql_text(1)||sql_text(2)||sql_text(3), user, v_systimestamp
        from dual;
  end if;

end ddl_trigger;
/

Since the number of 64-byte ‘pieces’ of the SQL text can be quite large the trigger restricts the SQL_TEXT column to the first three ‘pieces’, making the maximum length of the string 192 characters. As expected for larger statements the complete text will not be provided but it should capture any ‘alter table’ statements in their entirety. Note that this trigger will capture not only ALTER TABLE statements but also any CREATE/ALTER/DROP statement submitted to the database. This means that alter user, alter trigger, alter package, alter function, alter tablespace, alter system, create … and drop … statements also get logged in the DDL_LOG table. Because of this, the table may grow rapidly and become quite large, thus a plan for keeping a finite history should be created. For most systems, 90 days should be sufficient to track table changes in the database. Reports generated from the logged data can be kept for longer periods of time (for example, 12 months) before being removed.

A sample script for managing the table data is provided below; it enforces a 90-day window of data. A log directory is created:


mkdir -p /u01/app/oracle/ddl_chg/purge_logs

A SQL script is written to purge the old records from DDL_LOG:


column sys_date new_value dt noprint
column name new_value db_nm noprint
select to_char(sysdate,'RRRRMMDD') sys_date from dual;
select name from v$database;

spool /u01/app/oracle/ddl_chg/purge_logs/ddl_log_purge_$db_nm._&dt..log
set echo on

--
-- Records slated for removal
--
select * From ddl_log where attempt_dt < sysdate - 90;

--
-- Delete selected records
--
delete from ddl_log where attempt_dt < sysdate - 90;

commit;

spool off
set echo off

This, obviously, can’t run directly from cron (or any similar scheduler) so a wrapper script is needed:


#!/bin/ksh

#
# purge_ddl_log_90.sh
#
# Shell script to purge old audit records
# from the DDL_LOG table
#

#
# Find the selected database and set the environment
#
set -A database `ps -ef | grep [p]mon | grep '<name>' |  awk -F"_" '{print $3}'`

for i in ${database[@]}

#
# Set the environment for the database
#
do
        ORACLE_SID=$i
        export ORACLE_SID

        ORAENV_ASK=NO
        export ORAENV_ASK

        unset ORACLE_BASE
        export ORACLE_BASE

        PATH=$PATH:<ORACLE_HOME/bin location>

        . <ORACLE_HOME/bin>/oraenv -s

        LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLE_HOME/lib:$ORACLE_HOME/precomp/public
        export LD_LIBRARY_PATH

        PATH=$ORACLE_HOME/bin:$PATH
        export PATH

#
# Start SQL*Plus and execute the script
#
        sqlplus /nolog <<EOF
connect / as sysdba
@/u01/app/oracle/ddl_chg/purge_ddl_log_90.sql
EOF

done

#
# Make the output files readable for all
*
cd /u01/app/oracle/ddl_chg/purge_logs

chmod 666 *.log

#
# Remove old purge logs
#

find . -name "purge*log" -mtime +365 -exec /bin/rm -rf {} ;

The shell script sets the proper environment and ORACLE_SID based on the output of the ps command. The script will need to be edited to provide the database name to search for and the ORACLE_HOME location. More than one database name can be specified using | as a separator:


'abd|def|ghi|jkl'

This provides a way to purge the DDL_LOG table in every database where this table/trigger combination has been installed. The database name is included in the log file name to keep the purge trails separate for each database. The length of time to keep the logfiles can be changed to meet the storage limits of the system being monitored.

Change reports can be generated from the data found in the DDL_LOG table:


set linesize 140
column sdate new_value sdt noprint
select to_Char(sysdate, 'RRRRMMDDHH24')sdate from dual;

column modlen new_value mlen noprint
select 'a'||nvl(max(length(modification)),25) modlen From
(select obj_owner owner, object_name tabname, substr(sql_text, instr(sql_text, 'modify ')) modification, attempt_dt mod_time
from ddl_log
where (instr(sql_text, 'alter table') > 0
or instr(sql_text, 'ALTER TABLE') > 0));
column objlen new_value olen noprint
select 'a'||nvl(max(length(owner||'.'||tabname)),60) objlen From
(select obj_owner owner, object_name tabname, substr(sql_text, instr(sql_text, 'modify ')) modification, attempt_dt mod_time
from ddl_log
where (instr(sql_text, 'alter table') > 0
or instr(sql_text, 'ALTER TABLE') > 0));

column modification format &mlen
column mod_time format a29
column tab_name format &olen

select owner||'.'|| tabname tab_name, modification, mod_time
from
(select obj_owner owner, object_name tabname, substr(sql_text, instr(sql_text, 'add ')) modification, attempt_dt mod_time
from ddl_log
where instr(lower(sql_text), 'alter table') > 0
union
select obj_owner owner, object_name tabname, substr(sql_text, instr(sql_text, 'drop ')) modification, attempt_dt mod_time
from ddl_log
where instr(lower(sql_text), 'alter table') > 0
union
select obj_owner owner, object_name tabname, substr(sql_text, instr(sql_text, 'modify ')) modification, attempt_dt mod_time
from ddl_log
where instr(lower(sql_text), 'alter table') > 0
union
select obj_owner owner, object_name tabname, substr(sql_text, instr(sql_text, 'ADD ')) modification, attempt_dt mod_time
from ddl_log
where instr(lower(sql_text), 'alter table') > 0
union
select obj_owner owner, object_name tabname, substr(sql_text, instr(sql_text, 'DROP ')) modification, attempt_dt mod_time
from ddl_log
where instr(lower(sql_text), 'alter table') > 0
union
select obj_owner owner, object_name tabname, substr(sql_text, instr(sql_text, 'MODIFY ')) modification, attempt_dt mod_time
from ddl_log
where instr(lower(sql_text), 'alter table') > 0) dl
where lower(dl.modification) not like '%table%'
and mod_time >= trunc(systimestamp)
order by 1, 3

spool /u01/app/oracle/ddl_chg/log/tab_chg_rpt_&sdt._&1..lst
/
spool off

The database name is passed to the script so it will be included in the report file name. The code reports only on table changes (hence the long string of UNION queries) and produces a report similar to the one shown below:


TAB_NAME         MODIFICATION                   MOD_TIME
---------------- ------------------------------ -----------------------------
SCOTT.DDL_LOG    modify sql_text varchar2(200)  23-NOV-19 01.23.49.859971 PM

The script also sets the column formatting based on the maximum length of the stored data to possibly reduce the line length. Timestamp data was used in order to provide both date and visible time values for the generated change records. These scripts have been tested but might require some modifications based on the operating system vendor’s implementation of Linux/Unix.

For those DBAs not running replicated systems this might not be of much use. But, for those replicating data from Oracle to other systems (such as BigQuery, Snowflake and the like), knowing when table changes have occurred can make it easier to deal with replication failures created by those changes. The faster the replication process can get back on track the faster the systems that rely on that replicated data can return to functionality.

# # #

See articles by David Fitzjarrell

David Fitzjarrell
David Fitzjarrell
David Fitzjarrell has more than 20 years of administration experience with various releases of the Oracle DBMS. He has installed the Oracle software on many platforms, including UNIX, Windows and Linux, and monitored and tuned performance in those environments. He is knowledgeable in the traditional tools for performance tuning – the Oracle Wait Interface, Statspack, event 10046 and 10053 traces, tkprof, explain plan and autotrace – and has used these to great advantage at the U.S. Postal Service, American Airlines/SABRE, ConocoPhilips and SiriusXM Radio, among others, to increase throughput and improve the quality of the production system. He has also set up scripts to regularly monitor available space and set thresholds to notify DBAs of impending space shortages before they affect the production environment. These scripts generate data which can also used to trend database growth over time, aiding in capacity planning. He has used RMAN, Streams, RAC and Data Guard in Oracle installations to ensure full recoverability and failover capabilities as well as high availability, and has configured a 'cascading' set of DR databases using the primary DR databases as the source, managing the archivelog transfers manually and montoring, through scripts, the health of these secondary DR databases. He has also used ASM, ASMM and ASSM to improve performance and manage storage and shared memory.

Get the Free Newsletter!

Subscribe to Cloud Insider for top news, trends & analysis

Latest Articles