Questo articolo Γ¨ un copia e incolla dell’articolo ufficiale che trovate qui:
http://www.oracle.com/technetwork/issue-archive/2016/16-mar/o26performance-2925662.html
A Fresh Look at Auditing Row Changes
By Connor McDonald
Triggers can provide auditing information, but thereβs a future in flashback.
Barbara is looking forward to a long-overdue vacation.Β Sheβs been working extremely hard to get a suite of new features for the companyβs human resources (HR) application through construction, testing, and finally deployment into the production environment. Satisfaction ratings from the HR department are high, and no serious bugs have been reported.
But Barbaraβs vacation plans are about to be stymied. As often happens in IT projects, some requirements get pushed to the side during the construction phase, with the intent of tackling them later. Now, an email titled βUrgent: Need to capture all row changes for audit complianceβ arrives in Barbaraβs inbox. Audit functions, although important from a security and compliance perspective, are typically invisible to application stakeholders and are hardly the exciting part of any functionality showcase. So the auditing requirements for Barbaraβs application enhancements were pushed aside early in the construction phase, only to resurface as an emergency postimplementation requirement.
Fortunately for Barbara, auditing row changes is a common need throughout the IT industry. A quick web search reveals the most prevalent solution: for each table in her application, there should be a partnering audit table to hold the history of row changes, plus some additional metadata.
For example, for the EMPLOYEES table in Barbaraβs application:
SQL> desc EMPLOYEES
Name Null? Type
βββββββββββββββββββββββββββββ ββββββββ ββββββββββββ
EMPLOYEE_ID NUMBER(6)
FIRST_NAME VARCHAR2(20)
LAST_NAME NOT NULL VARCHAR2(25)
EMAIL NOT NULL VARCHAR2(25)
PHONE_NUMBER VARCHAR2(20)
HIRE_DATE NOT NULL DATE
JOB_ID NOT NULL VARCHAR2(10)
SALARY NUMBER(8,2)
MANAGER_ID NUMBER(6)
DEPARTMENT_ID NUMBER(4)
VACATION_BALANCE NUMBER(6,2)
there will be an appropriately named equivalent table for capturing any row changes:
SQL> desc AUDIT_EMPLOYEES
Name Null? Type
βββββββββββββββββββββββββββββ ββββββββ ββββββββββββ
AUD_WHO VARCHAR2(20)
AUD_WHEN DATE
AUD_OPERATION VARCHAR2(1)
AUD_MODULE VARCHAR2(30)
EMPLOYEE_ID NUMBER(6)
FIRST_NAME VARCHAR2(20)
LAST_NAME NOT NULL VARCHAR2(25)
EMAIL NOT NULL VARCHAR2(25)
PHONE_NUMBER VARCHAR2(20)
HIRE_DATE NOT NULL DATE
JOB_ID NOT NULL VARCHAR2(10)
SALARY NUMBER(8,2)
MANAGER_ID NUMBER(6)
DEPARTMENT_ID NUMBER(4)
VACATION_BALANCE NUMBER(6,2)
Some additional metadata columns are present in the AUDIT_EMPLOYEES table for capturing who performed the change, when the change occurred, what operation was performed (INSERT, UPDATE, or DELETE), and from which program or module the change originated. Barbaraβs application is already live in the production environment, so the function for populating the AUDIT_EMPLOYEES table must be transparent to the existing code. Thus, database triggers are the natural choice.
Listing 1Β shows the trigger Barbara has built to capture all changes to the EMPLOYEES table.
Code Listing 1:Β Audit data capture trigger for EMPLOYEES
SQL> create or replace
2 trigger TRG_AUDIT_EMPLOYEES
3 after insert or update or delete on EMPLOYEES
4 for each row
5 declare
6 l_operation varchar2(1) :=
7 case when updating then 'U'
8 when deleting then 'D'
9 else 'I' end;
10 begin
11 if updating or inserting then
12 insert into AUDIT_EMPLOYEES
13 (aud_who
14 ,aud_when
15 ,aud_operation
16 ,aud_module
17 ,employee_id
18 ,first_name
19 ,last_name
20 ,email
21 ,phone_number
22 ,hire_date
23 ,job_id
24 ,salary
25 ,commission_pct
26 ,manager_id
27 ,department_id)
28 values
29 (user
30 ,sysdate
31 ,l_operation
32 ,sys_context('USERENV','MODULE')
33 ,:new.employee_id
34 ,:new.first_name
35 ,:new.last_name
36 ,:new.email
37 ,:new.phone_number
38 ,:new.hire_date
39 ,:new.job_id
40 ,:new.salary
41 ,:new.commission_pct
42 ,:new.manager_id
43 ,:new.department_id);
44 else
45 insert into AUDIT_EMPLOYEES
46 (aud_who
47 ,aud_when
48 ,aud_operation
49 ,aud_module
50 ,employee_id
51 ,first_name
52 ,last_name
53 ,email
54 ,phone_number
55 ,hire_date
56 ,job_id
57 ,salary
58 ,commission_pct
59 ,manager_id
60 ,department_id)
61 values
62 (user
63 ,sysdate
64 ,l_operation
65 ,sys_context('USERENV','MODULE')
66 ,:old.employee_id
67 ,:old.first_name
68 ,:old.last_name
69 ,:old.email
70 ,:old.phone_number
71 ,:old.hire_date
72 ,:old.job_id
73 ,:old.salary
74 ,:old.commission_pct
75 ,:old.manager_id
76 ,:old.department_id);
77 end if;
78 end;
79 /
Trigger created.
A quick series of tests confirms the correct operation of the trigger, so Barbara forges ahead and creates auditing tables and associated triggers for all the tables in the HR application.
Hidden Costs
Barbaraβs vacation plans remind her that one of the tasks performed each night on the EMPLOYEES table is an update of the vacation balance for all employees in the organization. Before her auditing changes went live in the production environment, this task was efficientβit had to be, because it temporarily locks all rows in the EMPLOYEES table.
SQL> update EMPLOYEES
2 set VACATION_BALANCE = VACATION_BALANCE + 1;
219136 rows updated.
Elapsed: 00:00:03.01
A few seconds is an acceptable lock time. In the rare instance that anyone updates a single employee record outside of business hours, that person would experience only a slight pauseβnothing that would be cause for alarm. But now that Barbaraβs auditing changes have been implemented, a problem has surfaced. The vacation adjustment takes significantly longer:
SQL> update EMPLOYEES
2 set VACATION_BALANCE = VACATION_BALANCE + 1;
219136 rows updated.
Elapsed: 00:00:17.92
Barbara performs a trace on the vacation update to gauge the impact of auditing.Β Listing 2Β shows the TKprof-formatted output of the trace file.
Code Listing 2:Β TKprof output from trace of vacation adjustment
update EMPLOYEES
set VACATION_BALANCE = VACATION_BALANCE + 1
call count cpu elapsed disk query current rows
βββββββ ββββββ ββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ
Parse 1 0.00 0.00 0 1 0 0
Execute 1 15.61 17.47 0 2380 223618 219136
Fetch 0 0.00 0.00 0 0 0 0
βββββββ ββββββ ββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ
total 2 15.61 17.48 0 2381 223618 219136
INSERT INTO AUDIT_EMPLOYEES (AUD_WHO ,AUD_WHEN ,AUD_OPERATION ,AUD_MODULE ,
EMPLOYEE_ID ,FIRST_NAME ,LAST_NAME ,EMAIL ,PHONE_NUMBER ,HIRE_DATE ,JOB_ID ,
SALARY ,MANAGER_ID ,DEPARTMENT_ID ,VACATION_BALANCE)
VALUES
(USER ,SYSDATE ,:B12 ,SYS_CONTEXT('USERENV','MODULE') ,:B1 ,:B2 ,:B3 ,:B4 ,
:B5 ,:B6 ,:B7 ,:B8 ,:B9 ,:B10 ,:B11 )
call count cpu elapsed disk query current rows
βββββββ ββββββ ββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ
Parse 1 0.00 0.00 0 0 0 0
Execute 219136 8.60 8.71 0 5865 245856 219136
Fetch 0 0.00 0.00 0 0 0 0
βββββββ ββββββ ββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ
total 219137 8.60 8.71 0 5865 245856 219136
From the trace file, Barbara observes that the INSERT statement for the audit table added 8.71 seconds of elapsed time to the process. It was called 219,136 timesβonce for every changed row, even though only a single UPDATE statement was issued. This also incurs the same number ofΒ context switchesΒ between the SQL and PL/SQL engines, further adding to the overall elapsed time. Barbaraβs auditing changes are not as transparent to the application as they initially appeared to be. She needs a fixβ¦fast.
To reduce the context switching, Barbara must refactor the audit trigger code to use bulk binding. This is not a trivial undertaking, because row changes need to be buffered as they occur and then applied in one pass after the issuing data manipulation language (DML) is complete. In earlier versions of Oracle Database, this would mandate a PACKAGE definition along withΒ threeΒ separate triggers. But as of Oracle Database 11g, a singleΒ compoundΒ trigger can be used to provide the required functionality.
Listing 3Β shows the compound trigger Barbara built to improve the efficiency of her audit capture function.
Code Listing 3:Β Compound trigger for audit capture
SQL> create or replace
2 trigger TRG_AUDIT_EMPLOYEES
3 for insert or update or delete
4 on EMPLOYEES compound trigger
5
6 --
7 -- an array structure to buffer all the row changes
8 --
9 type t_row_list is
10 table of AUDIT_EMPLOYEES%rowtype index by pls_integer;
11
12 l_audit_rows t_row_list;
13
14 l_operation varchar2(1) :=
15 case when updating then 'U'
16 when deleting then 'D'
17 else 'I' end;
18
19 before statement is
20 begin
21 --
22 -- initialize the array
23 --
24 l_audit_rows.delete;
25 end before statement;
26
27 after each row is
28 begin
29 --
30 -- at row level, capture all the changes into the array
31 --
32 l_audit_rows(l_audit_rows.count+1).aud_who := sys_context('USERENV','SESSION_USER');
33 l_audit_rows(l_audit_rows.count).aud_when := sysdate;
34 l_audit_rows(l_audit_rows.count).aud_operation := l_operation;
35 l_audit_rows(l_audit_rows.count).aud_module := sys_context('USERENV','MODULE');
36
37 if updating or inserting then
38 l_audit_rows(l_audit_rows.count).employee_id := :new.employee_id;
39 l_audit_rows(l_audit_rows.count).first_name := :new.first_name;
40 l_audit_rows(l_audit_rows.count).last_name := :new.last_name;
41 l_audit_rows(l_audit_rows.count).email := :new.email;
42 l_audit_rows(l_audit_rows.count).phone_number := :new.phone_number;
43 l_audit_rows(l_audit_rows.count).hire_date := :new.hire_date;
44 l_audit_rows(l_audit_rows.count).job_id := :new.job_id;
45 l_audit_rows(l_audit_rows.count).salary := :new.salary;
46 l_audit_rows(l_audit_rows.count).manager_id := :new.manager_id;
47 l_audit_rows(l_audit_rows.count).department_id := :new.department_id;
48 l_audit_rows(l_audit_rows.count).vacation_balance := :new.vacation_balance;
49 else
50 l_audit_rows(l_audit_rows.count).employee_id := :old.employee_id;
51 l_audit_rows(l_audit_rows.count).first_name := :old.first_name;
52 l_audit_rows(l_audit_rows.count).last_name := :old.last_name;
53 l_audit_rows(l_audit_rows.count).email := :old.email;
54 l_audit_rows(l_audit_rows.count).phone_number := :old.phone_number;
55 l_audit_rows(l_audit_rows.count).hire_date := :old.hire_date;
56 l_audit_rows(l_audit_rows.count).job_id := :old.job_id;
57 l_audit_rows(l_audit_rows.count).salary := :old.salary;
58 l_audit_rows(l_audit_rows.count).manager_id := :old.manager_id;
59 l_audit_rows(l_audit_rows.count).department_id := :old.department_id;
60 l_audit_rows(l_audit_rows.count).vacation_balance := :old.vacation_balance;
61 end if;
62 end after each row;
63
64 after statement is
65 begin
66 --
67 -- then at completion, do a single insert of all the rows into our audit table
68 --
69
70 forall i in 1 .. l_audit_rows.count
71 insert into AUDIT_EMPLOYEES
72 values l_audit_rows(i);
73 l_audit_rows.delete;
74 end after statement;
75
76 end;
77 /
Trigger created.
Barbara re-examines the performance of the vacation adjustment with her new trigger in place.
SQL> update EMPLOYEES
2 set VACATION_BALANCE = VACATION_BALANCE + 1;
219136 rows updated.
Elapsed: 00:00:04.01
Her audit capture now imposes dramatically reduced overhead. A trace of the process confirms the benefit of bulk binding in this instance.Β Listing 4Β shows that the number of calls to INSERT into the AUDIT_EMPLOYEES table has shrunk from more than 200,000 down to the optimal value, 1.
Code Listing 4:Β TKprof output from trace of vacation adjustment, with new compound trigger
update EMPLOYEES
set VACATION_BALANCE = VACATION_BALANCE + 1
call count cpu elapsed disk query current rows
βββββββ ββββββ ββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ
Parse 1 0.01 0.00 0 1 0 0
Execute 1 3.41 3.47 0 2365 224004 219136
Fetch 0 0.00 0.00 0 0 0 0
βββββββ ββββββ ββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ
total 2 3.43 3.47 0 2366 224004 219136
INSERT INTO AUDIT_EMPLOYEES
VALUES
(:B1 ,:B2 ,:B3 ,:B4 ,:B5 ,:B6 ,:B7 ,:B8 ,:B9 ,:B10 ,:B11 ,:B12 ,:B13 ,:B14 ,
:B15 )
call count cpu elapsed disk query current rows
βββββββ ββββββ ββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.43 0.51 0 3306 21864 219136
Fetch 0 0.00 0.00 0 0 0 0
βββββββ ββββββ ββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ ββββββββββ
total 2 0.43 0.51 0 3306 21864 219136
Barbaraβs auditing enhancements to the HR application are back on track, as are her vacation plans. Still, Barbara has a nagging concern about the amount of potential code maintenance overhead she has introduced. In the future, every time a structural change is made to the HR application, the auditing triggers will need to be refactored to accommodate that change. And Barbaraβs fix does not address another potential pitfall with the trigger-based solution: although bulk binding improves performance and resolves the context-switching overhead, it also introduces the risk of excessive memory consumption. Because all rows changed by the issuing DML are buffered before being processed at statement completion, a significant amount of session memory can be consumed. This could strain the database serverβor, worse, it could crash the application session with βORA-4030: Out of process memoryβ errors.
Listing 5Β shows the memory consumption Barbaraβs bulk binding incurs when the vacation adjustment runs. The session requires more than 160 MB of program global area (PGA) memory. This may be acceptable for a single session running a nightly process such as the vacation update, but it obviously would not scale to a scenario where hundreds or thousands of sessions have a similar requirement.
Code Listing 5:Β Session memory consumption for the vacation adjustment with bulk binding
--
-- First we check the current ceiling of PGA memory before
-- the vacation adjustment
--
SQL> select st.sid, s.name, st.value
2 from v$statname s, v$sesstat st
3 where st.statistic# = s.statistic#
4 and st.sid = sys_context('USERENV','SID')
5 and s.name = 'session pga memory max';
SID NAME VALUE
ββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββββββββ βββββββ
6 session pga memory max 2508344
1 row selected.
SQL> update EMPLOYEES
2 set VACATION_BALANCE = VACATION_BALANCE + 1;
219136 rows updated.
--
-- Then we check the same statistics once the adjustment has completed
--
SQL> select st.sid, s.name, st.value
2 from v$statname s, v$sesstat st
3 where st.statistic# = s.statistic#
4 and st.sid = sys_context('USERENV','SID')
5 and s.name = 'session pga memory max';
SID NAME VALUE
ββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββββββββ βββββββ
6 session pga memory max 164430744
To obviate that risk, the trigger can be adjusted to perform bulk binding in batches throughout the DML processing, as shown inΒ Listing 6. But with both code maintenance overhead and complexity increasing, Barbara is keen to find a simpler solution.
Code Listing 6:Β Trigger to keep PGA consumption in check, still using bulk binding
SQL> create or replace
2 trigger TRG_AUDIT_EMPLOYEES
3 for insert or update or delete
4 on EMPLOYEES compound trigger
5
6 --
7 -- an array structure to buffer all the row changes
8 --
9 type t_row_list is
10 table of AUDIT_EMPLOYEES%rowtype index by pls_integer;
11
12 l_audit_rows t_row_list;
13
14 l_operation varchar2(1) :=
15 case when updating then 'U'
16 when deleting then 'D'
17 else 'I' end;
18
19 procedure insert_logged_so_far is
20 begin
21 forall i in 1 .. l_audit_rows.count
22 insert into AUDIT_EMPLOYEES
23 values l_audit_rows(i);
24 l_audit_rows.delete;
25 end;
26
27 before statement is
28 begin
29 --
30 -- initialize the array
31 --
32 l_audit_rows.delete;
33 end before statement;
34
35 after each row is
36 begin
37 --
38 -- at row level, capture all the changes into the array
39 --
40 l_audit_rows(l_audit_rows.count+1).aud_who := sys_context('USERENV','SESSION_USER');
41 l_audit_rows(l_audit_rows.count).aud_when := sysdate;
42 l_audit_rows(l_audit_rows.count).aud_operation := l_operation;
43 l_audit_rows(l_audit_rows.count).aud_module := sys_context('USERENV','MODULE');
44
45 if updating or inserting then
46 l_audit_rows(l_audit_rows.count).employee_id := :new.employee_id;
47 l_audit_rows(l_audit_rows.count).first_name := :new.first_name;
48 l_audit_rows(l_audit_rows.count).last_name := :new.last_name;
49 l_audit_rows(l_audit_rows.count).email := :new.email;
50 l_audit_rows(l_audit_rows.count).phone_number := :new.phone_number;
51 l_audit_rows(l_audit_rows.count).hire_date := :new.hire_date;
52 l_audit_rows(l_audit_rows.count).job_id := :new.job_id;
53 l_audit_rows(l_audit_rows.count).salary := :new.salary;
54 l_audit_rows(l_audit_rows.count).manager_id := :new.manager_id;
55 l_audit_rows(l_audit_rows.count).department_id := :new.department_id;
56 l_audit_rows(l_audit_rows.count).vacation_balance := :new.vacation_balance;
57 else
58 l_audit_rows(l_audit_rows.count).employee_id := :old.employee_id;
59 l_audit_rows(l_audit_rows.count).first_name := :old.first_name;
60 l_audit_rows(l_audit_rows.count).last_name := :old.last_name;
61 l_audit_rows(l_audit_rows.count).email := :old.email;
62 l_audit_rows(l_audit_rows.count).phone_number := :old.phone_number;
63 l_audit_rows(l_audit_rows.count).hire_date := :old.hire_date;
64 l_audit_rows(l_audit_rows.count).job_id := :old.job_id;
65 l_audit_rows(l_audit_rows.count).salary := :old.salary;
66 l_audit_rows(l_audit_rows.count).manager_id := :old.manager_id;
67 l_audit_rows(l_audit_rows.count).department_id := :old.department_id;
68 l_audit_rows(l_audit_rows.count).vacation_balance := :old.vacation_balance;
69 end if;
70
71 --
72 -- bulk bind every 1000 rows to keep the memory down
73 --
74 if l_audit_rows.count > 1000 then
75 insert_logged_so_far;
76 end if;
77 end after each row;
78
79 after statement is
80 begin
81 --
82 -- then at completion, pick up the remaining rows
83 --
84 if l_audit_rows.count > 0 then
85 insert_logged_so_far;
86 end if;
87 end after statement;
88
89 end;
90 /
Trigger created.
SQL> select st.sid, s.name, st.value
2 from v$statname s, v$sesstat st
3 where st.statistic# = s.statistic#
4 and st.sid = sys_context('USERENV','SID')
5 and s.name = 'session pga memory max';
SID NAME VALUE
ββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββββββββ βββββββ
6 session pga memory max 2508344
1 row selected.
SQL> update EMPLOYEES
2 set VACATION_BALANCE = VACATION_BALANCE + 1;
219136 rows updated.
Elapsed: 00:00:05.31
SQL> select st.sid, s.name, st.value
2 from v$statname s, v$sesstat st
3 where st.statistic# = s.statistic#
4 and st.sid = sys_context('USERENV','SID')
5 and s.name = 'session pga memory max';
SID NAME VALUE
ββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββββββββ βββββββ
6 session pga memory max 2508344
1 row selected.
A Better Way
The use of triggers for capturing row changes is common throughout the Oracle application development community. But few developers display Barbaraβs diligence in ensuring that bulk binding caters to multirow DML statements, and even fewer explore potential alternatives.
Barbara peruses theΒ Oracle Database Advanced Application Developerβs GuideΒ and comes across Oracle Flashback technology. The Flashback Query feature catches her eye:Β βOracle Flashback Query retrieves data as it existed at some time in the past.βΒ SinceΒ anyΒ time could be nominated, Barbara infers that it should be doable to see every possible state of a database table, which sounds very much like the auditing requirement she has just implemented. In the same section of the documentation, she also discovers Oracle Flashbackβs Oracle Flashback Data Archive feature:Β βFlashback Data Archive provides the ability to track and store all transactional changes to a table over its lifetime. It is no longer necessary to build this intelligence into your application. A Flashback Data Archive is useful for compliance with record stage policies and audit reports.βΒ This sounds like a perfect match for Barbaraβs audit needs, so she builds a Flashback Data Archive test case to examine its features.
First she allocates some storage to hold her archive data:
SQL> create tablespace SPACE_FOR_ARCHIVE
2 datafile 'C:\ORACLE\DB12\ARCH_SPACE.DBF'
3 size 50G;
Tablespace created.
Next, she creates a new structure, called aΒ flashback archive, which defines the retention duration of the row change history for any table placed into it.
SQL> CREATE FLASHBACK ARCHIVE longterm
2 TABLESPACE space_for_archive
3 RETENTION 1 YEAR;
Flashback archive created
And then she simply associates her HR tables with the flashback archive.
SQL> ALTER TABLE EMPLOYEES FLASHBACK ARCHIVE LONGTERM;
Table altered.
βSo far, so good,β thinks Barbara. However, it is not immediately apparent where the audited row changes are located or how she is meant to access them.Β Listing 7Β shows that some new objects have been created within Barbaraβs schema. Their structure suggests they are related to Flashback Data Archive in some way, but she finds it unlikely that sheβs supposed to query those tables directly.
Code Listing 7:Β Tables created to support Flashback Data Archive
SQL> select * from tab;
TNAME TABTYPE CLUSTERID
ββββββββββββββββββββββββββββββββββββββββ βββββββ βββββββββ
EMPLOYEES TABLE
SYS_FBA_DDL_COLMAP_102596 TABLE
SYS_FBA_HIST_102596 TABLE
SYS_FBA_TCRV_102596 TABLE
SQL> desc SYS_FBA_DDL_COLMAP_102596
Name Null? Type
βββββββββββββββββββββββββββββ ββββββββ ββββββββββββββββββ
STARTSCN NUMBER
ENDSCN NUMBER
XID RAW(8)
OPERATION VARCHAR2(1)
COLUMN_NAME VARCHAR2(255)
TYPE VARCHAR2(255)
HISTORICAL_COLUMN_NAME VARCHAR2(255)
SQL> desc SYS_FBA_HIST_102596
Name Null? Type
βββββββββββββββββββββββββββββ ββββββββ ββββββββββββββββββ
RID VARCHAR2(4000)
STARTSCN NUMBER
ENDSCN NUMBER
XID RAW(8)
OPERATION VARCHAR2(1)
EMPLOYEE_ID NUMBER(6)
FIRST_NAME VARCHAR2(20)
LAST_NAME VARCHAR2(25)
EMAIL VARCHAR2(25)
PHONE_NUMBER VARCHAR2(20)
HIRE_DATE DATE
JOB_ID VARCHAR2(10)
SALARY NUMBER(8,2)
MANAGER_ID NUMBER(6)
DEPARTMENT_ID NUMBER(4)
VACATION_BALANCE NUMBER(6,2)
SQL> desc SYS_FBA_TCRV_102596
Name Null? Type
βββββββββββββββββββββββββββββ ββββββββ ββββββββββββββββββ
RID VARCHAR2(4000)
STARTSCN NUMBER
ENDSCN NUMBER
XID RAW(8)
OP VARCHAR2(1)
To determine whether the flashback archive is working as expected, Barbara tries a test query examining the EMPLOYEES table at a past point in time. The execution plan inΒ Listing 8Β confirms her hypothesis that the new tables that were created are related to Flashback Data Archive and are accessing historical data.
Code Listing 8:Β Execution plan for tables associated with Flashback Data Archive
SQL> set autotrace traceonly explain
SQL> select * from EMPLOYEES AS OF TIMESTAMP SYSDATE-3;
Execution Plan
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Id | Operation | Name | Rows |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| 0 | SELECT STATEMENT | | 9399 |
| 1 | VIEW | | 9399 |
| 2 | UNION-ALL | | |
|* 3 | FILTER | | |
| 4 | PARTITION RANGE SINGLE| | 1054 |
|* 5 | TABLE ACCESS FULL | SYS_FBA_HIST_102596 | 1054 |
|* 6 | FILTER | | |
| 7 | MERGE JOIN OUTER | | 8345 |
| 8 | SORT JOIN | | 203 |
|* 9 | TABLE ACCESS FULL | EMPLOYEES | 203 |
|* 10 | SORT JOIN | | 230K|
|* 11 | TABLE ACCESS FULL | SYS_FBA_TCRV_102596 | 230K|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Seeing a point-in-time state of the EMPLOYEES table is useful, but Barbara needs more than that. She needs to be able to seeΒ everyΒ point-in-time state of a row in the table to build up a history of changes and satisfy her audit requirements. With each update to the table, a new βversionβ of a row comes into existence, and Barbara needs to list every one of those versions. Having reviewed Oracle Flashback technology features in the documentation, Barbara knows that she can use the Flashback Version Query feature of Oracle Flashback to achieve this.Β Listing 9Β shows how the VERSIONS BETWEEN syntax lists all the historical versions of a single employee record (EMPLOYEE_ID = 100) and how the VERSIONS_STARTTIME and VERSIONS_OPERATION pseudocolumns can be used to track the transaction time and operation performed on each row.
Code Listing 9:Β Flashback Version Query
SQL> select EMPLOYEE_ID, FIRST_NAME, JOB_ID, VACATION_BALANCE,
2 VERSIONS_STARTTIME TS,
3 nvl(VERSIONS_OPERATION,'I') OP
4 from EMPLOYEES
5 versions between timestamp timestamp '2016-01-11 08:20:00' and systimestamp
6 where EMPLOYEE_ID = 100
7 order by EMPLOYEE_ID, ts;
EMPLOYEE_ID FIRST_NAME JOB_ID VACATION_BALANCE TS O
βββββββββββ ββββββββββββββββββ ββββββββββ ββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
100 Steven AD_PRES 45.76 11-JAN-16 08.27.55.000000000 AM U
100 Steven AD_PRES 46.76 12-JAN-16 08.28.07.000000000 AM U
100 Steven AD_PRES 47.76 13-JAN-16 08.53.56.000000000 AM U
100 Steven AD_PRES 48.76 14-JAN-16 08.53.56.000000000 AM U
100 Steven AD_PRES 49.76 15-JAN-16 08.53.56.000000000 AM U
Barbara is excited about her progress. She can now list a history of row changes for the EMPLOYEES table, and no triggers are required. The vacation update routine will run at the same swift speed as it did before her audit changes were implemented.
But there is still an outstanding issue. Barbaraβs original trigger-based audit implementation picked up additional metadataβnamely,Β whoΒ did the change andΒ whatΒ program was being executed. That data is not present in the Flashback Data Archive tables. Barbara doesnβt want to add those metadata columns to each of the base tables in her application, because that would break a primary implementation requirement for auditingβthat it should beΒ transparentΒ to the existing application.
Luckily for Barbara, her application is deployed on Oracle Database 12c. Flashback Data Archive has been extended to enable the capture of context-level information automatically and without changes to the base tables. Barbara contacts her administrator and requests context-level tracking to be enabled for Flashback Data Archive:
SQL> exec dbms_flashback_archive.set_context_level('ALL');
PL/SQL procedure successfully completed.
Now, when rows are inserted, updated, or deleted in the tables Barbara has associated with a flashback archive, any context information in the standard USERENV context (as well as any custom user-defined contexts) will be captured for each transaction. From her trigger-based audit implementation, Barbara knows that she needs the MODULE and SESSION_USER attributes from the USERENV context. She performs some sample DMLs against the EMPLOYEES table and then checks to ensure that the context-level metadata is being collected.Β Listing 10Β shows that the capture of context information for each transaction is working and Barbaraβs implementation of the audit requirement with Flashback Data Archive is complete.
Code Listing 10:Β Flashback Version Query with context information
SQL> select EMPLOYEE_ID, FIRST_NAME, VACATION_BALANCE,
2 VERSIONS_STARTTIME TS,
3 dbms_flashback_archive.get_sys_context(versions_xid, 'USERENV','SESSION_USER') AS who,
4 dbms_flashback_archive.get_sys_context(versions_xid, 'USERENV','MODULE') AS program
5 from EMPLOYEES
6 versions between timestamp timestamp '2016-01-11 08:20:00' and systimestamp
7 where EMPLOYEE_ID = 100
8 order by EMPLOYEE_ID, ts;
EMPLOYEE_ID FIRST_NAME VACATION_BALANCE TS WHO PROGRAM
βββββββββββ ββββββββββββ ββββββββββββββββ ββββββββββββββββββββββ ββββββββββββ ββββββββ
100 Steven 52.76 11-JAN-16 10.04.03 BARBARA SQL*Plus
100 Steven 51.76 11-JAN-16 11.14.03 BARBARA SQL*Plus
100 Steven 53.76 11-JAN-16 12.05.06 BARBARA SQL*Plus
100 Steven 54.76 11-JAN-16 13.01.06 BARBARA SQL*Plus
100 Steven 55.76 11-JAN-16 14.23.09 BARBARA SQL*Plus
Although Barbara initially implemented the auditing requirements with a trigger-based approach, she does not need to abandon the audited row changes she has already collected. Using the DBMS_FLASHBACK_ARCHIVE package, she can import her captured audit changes into the new Flashback Data Archive tables and then drop her old auditing tables and triggers.
Conclusion
The use of triggers for auditing is common and is still often regarded as the only possible solution for capturing row changes on Oracle Database tables. However, in Oracle Database 11.2.0.4 and above, auditing requirements can now be met with Flashback Data Archiveβa simple, more efficient, and more secure solution.
Mi piace:
Mi piace Caricamento...