Monitoring changes in database tables data is always a pain in the ass for many developers. It was even for me until recently, when I had to create a dozen of tables which should have save history of changes. Saving the history was not a big deal but how to show differences in a clear (and easy as possible way) was a real challenge for me. I was able to do hard codding for each table, but I know that must be done in a more comfort way.
First have to point that browsing history is always related on one record an its data changes. If you have in mind that many columns may be changed in one update as well as many changes might be done on the same record in several attempts, finding only which data has been changed and present it in very clear way was a real problem.
I know that all presented here could be done much better and more "proffi" but in mine case it works ... fast and with no bugs. Because solution use analytic functions, only 10g and on wards database are supported.
Create demo environment
Demo environment will be based on famous EMP table who will have only 4 fields for easines of presenting main points.Regardless EMP table is the main, I'll start example with EMP_HIS table, which is history table for EMP table. History table means that this table will hold how records were looking before change (delete) action takes place in parent, EMP table. Because EMP table has built in triggers which reference EMP_HIS table, this table is shown first. Here is the whole create table script:
DROP TABLE EMP_HIS CASCADE CONSTRAINTS; CREATE TABLE EMP_HIS ( ID NUMBER NOT NULL, HIS_ACTION VARCHAR2(1 CHAR), HIS_DATE DATE, HIS_USER VARCHAR2(32 CHAR), ENAME VARCHAR2(10), HIREDATE DATE, SAL NUMBER(7,2), CREATE_DATE DATE NOT NULL, CREATE_USER VARCHAR2(32 CHAR) NOT NULL, EDIT_DATE DATE, EDIT_USER VARCHAR2(32 CHAR) ) ;This table is allmost the same as EMP with additional three fields (which are highlighted). This table should not have any index (or relation on EMP table) because history must not interfere with EMP table which is important only!
OK, let us go to mentioned, EMP table definition:
ALTER TABLE EMP DROP PRIMARY KEY CASCADE; DROP TABLE EMP CASCADE CONSTRAINTS; CREATE TABLE EMP ( ID NUMBER NOT NULL, ENAME VARCHAR2(10 CHAR), HIREDATE DATE, SAL NUMBER(7,2), CREATE_DATE DATE NOT NULL, CREATE_USER VARCHAR2(32 CHAR) NOT NULL, EDIT_DATE DATE, EDIT_USER VARCHAR2(32 CHAR) );Next, I'll add primary key-in mine case this is ID column (I adore surrogate PK's). Have to accent importance of having primary key Otherwise there is no way to reference master record (ROWID may be changed on table move or rebuild process!). Another reasonable restriction is that in this solution primary key CANNOT be modified.
ALTER TABLE EMP ADD ( CONSTRAINT EMP_PK PRIMARY KEY (ID) USING INDEX EMP_PK ENABLE VALIDATE );At the end add two triggers to EMP table. "before insert", which will assign only create_date and create_user fields (based on insert actions):
CREATE OR REPLACE TRIGGER EMP_TGBI BEFORE INSERT ON EMP REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW begin :new.CREATE_DATE:=sysdate; :new.CREATE_USER:=USER; end; /And "before update/delete" trigger which will be fired on update/delete actions on EMP table, with purpose to save original records to EMP_HIS table before they are changed or deleted. Here is its declaration:
CREATE OR REPLACE TRIGGER EMP_TGBUD BEFORE DELETE OR UPDATE ON EMP REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW DECLARE l_his_action VARCHAR2(1 CHAR); l_user VARCHAR2(32 CHAR); l_date DATE; BEGIN l_date:=sysdate; l_user:=USER; IF DELETING THEN l_his_action := 'D'; ELSIF UPDATING THEN l_his_action := 'U'; :new.EDIT_DATE := l_date; :new.EDIT_USER := l_user; END IF; INSERT INTO EMP_HIS ( ID, HIS_ACTION, HIS_DATE, HIS_USER, ENAME, HIREDATE, SAL, CREATE_DATE, CREATE_USER, EDIT_DATE, EDIT_USER ) VALUES ( :old.ID, l_his_action,l_date,l_user, :old.ENAME, :old.HIREDATE, :old.SAL, :old.CREATE_DATE, :old.CREATE_USER, :old.EDIT_DATE, :old.EDIT_USER ) ; END; /Now when tables are ready, let's add some records in EMP table:
truncate table EMP drop storage; INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7369,'SMITH',to_date('17-12-1980','dd-mm-yyyy'),800); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7499,'ALLEN',to_date('20-02-1981','dd-mm-yyyy'),1600); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7521,'WARD',to_date('22-02-1981','dd-mm-yyyy'),1250); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7566,'JONES',to_date('02-04-1981','dd-mm-yyyy'),2975); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7654,'MARTIN',to_date('28-09-1981','dd-mm-yyyy'),1250); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7698,'BLAKE',to_date('01-05-1981','dd-mm-yyyy'),2850); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7782,'CLARK',to_date('09-06-1981','dd-mm-yyyy'),2450); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7788,'SCOTT',to_date('13-07-1987','dd-mm-yyyy'),3000); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7839,'KING',to_date('17-11-1981','dd-mm-yyyy'),5000); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7844,'TURNER',to_date('08-09-1981','dd-mm-yyyy'),1500); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7876,'ADAMS',to_date('13-07-1987','dd-mm-yyyy'),1100); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7900,'JAMES',to_date('03-12-1981','dd-mm-yyyy'),950); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7902,'FORD',to_date('03-12-1981','dd-mm-yyyy'),3000); INSERT INTO EMP (ID,ENAME,HIREDATE,SAL) VALUES (7934,'MILLER',to_date('23-01-1982','dd-mm-yyyy'),1300); COMMIT;Let us modified some data to have some history:
UPDATE EMP SET SAL=900 WHERE ID=7369;And after succesfull update, old values are preserved in EMP_HIS table. Just for more consistent image, I'm altering session date value. And here is initial content of EMP_HIS table :
SQL> set linesize 140 SQL> alter session set nls_date_format='dd.mm.yyyy hh24:mi:ss'; Session altered. SQL> col id for 9990 SQL> col HIS_USER for a10 SQL> col CREATE_USER for a10 SQL> col EDIT_USER for a10 SQL> SELECT * FROM EMP_HIS; ID H HIS_DATE HIS_USER ENAME HIREDATE SAL CREATE_DATE CREATE_USE EDIT_DATE EDIT_USER ----- - ------------------- ---------- ---------- ------------------- ------ ------------------- ---------- ------------------- ---------- 7369 U 26.01.2012 14:58:32 TAB_APEX SMITH 17.12.1980 00:00:00 800 26.01.2012 14:56:50 TAB_APEX SQL>Here are brief explanation of first 4 columns:
- ID column shows us foreign key relation to PK in EMP table (EMP.ID column).
- HIS_ACTION column (represented as "H"), which may be D(elete) or U(pdate), tells us what action was performed.
- HIS_DATE is the date when this change happened (intentionally added to distinct previously saved edit_date value which might have value)
- HIS_USER is the user who has make that change. This field was needed to distinct previously saved edit_user value which might have different value.
SQL> UPDATE EMP SET SAL=1100 WHERE ID=7369; 1 row updated. SQL> UPDATE EMP SET ENAME='MR. SMITH' WHERE ID=7369; 1 row updated. SQL> commit; Commit complete. SQL>Let us see again EMP_HIS table:
SQL> SELECT * FROM EMP_HIS; ID H HIS_DATE HIS_USER ENAME HIREDATE SAL CREATE_DATE CREATE_USE EDIT_DATE EDIT_USER ----- - ------------------- ---------- ---------- ------------------- ------ ------------------- ---------- ------------------- ---------- 7369 U 26.01.2012 14:58:32 TAB_APEX SMITH 17.12.1980 00:00:00 800 26.01.2012 14:56:50 TAB_APEX 7369 U 26.01.2012 15:11:46 TAB_APEX SMITH 17.12.1980 00:00:00 900 26.01.2012 14:56:50 TAB_APEX 26.01.2012 14:58:32 TAB_APEX 7369 U 26.01.2012 15:11:46 TAB_APEX SMITH 17.12.1980 00:00:00 1100 26.01.2012 14:56:50 TAB_APEX 26.01.2012 15:11:46 TAB_APEXAs you see three records are present (one for each command). And for the end add two changes in one statement to same record. Return name to "SMITH" and rise again salary (good worker!):
SQL> UPDATE EMP SET ENAME='SMITH', SAL=1200 WHERE ID=7369; 1 row updated. SQL> commit; Commit complete. SQL>And look again in EMP_HIS table to see the content:
SQL> SELECT * FROM EMP_HIS; ID H HIS_DATE HIS_USER ENAME HIREDATE SAL CREATE_DATE CREATE_USE EDIT_DATE EDIT_USER ----- - ------------------- ---------- ---------- ------------------- ------ ------------------- ---------- ------------------- ---------- 7369 U 26.01.2012 14:58:32 TAB_APEX SMITH 17.12.1980 00:00:00 800 26.01.2012 14:56:50 TAB_APEX 7369 U 26.01.2012 15:11:46 TAB_APEX SMITH 17.12.1980 00:00:00 900 26.01.2012 14:56:50 TAB_APEX 26.01.2012 14:58:32 TAB_APEX 7369 U 26.01.2012 15:11:46 TAB_APEX SMITH 17.12.1980 00:00:00 1100 26.01.2012 14:56:50 TAB_APEX 26.01.2012 15:11:46 TAB_APEX 7369 U 26.01.2012 15:21:10 TAB_APEX MR. SMITH 17.12.1980 00:00:00 1100 26.01.2012 14:56:50 TAB_APEX 26.01.2012 15:11:46 TAB_APEXFour rows are now present in EMP_HIS table, what confirms that rows are added on statement levele not independant change of columns in table. This propertiy will be essential when we come to codding how to show change history. If you look closely it is very hard to see where changes occur and imagine a table with many fields ... almost impossible task for real human eye.
History GTT table
Now that we have real data examples it is time to add some aditional objects, required for this solution. First is EMP_HIS_GTT (global temporary table) which will have the same structure as EMP_HIS table. Idea to have GTT table is based on fact that history tables might have huge number of records and because they are not indexed in any way (to speed up DML on parent table), querying them without control may lead to performance problem. So EMP_HIS_GTT table will hold in one moment only data for select record from EMP table and all other actions will be performed on GTT not HIS table. Let me tell that in mine case I found the most suitable CACHE option enabled for GTT tables, but in your case this might differ-test! Here is the creation script for EMP_HIS_GTT global temporary table:DROP TABLE EMP_HIS_GTT CASCADE CONSTRAINTS; CREATE GLOBAL TEMPORARY TABLE EMP_HIS_GTT ( ID NUMBER NOT NULL, HIS_ACTION VARCHAR2(1 CHAR), HIS_DATE DATE, HIS_USER VARCHAR2(32 CHAR), ENAME VARCHAR2(10), HIREDATE DATE, SAL NUMBER(7,2), CREATE_DATE DATE NOT NULL, CREATE_USER VARCHAR2(32 CHAR) NOT NULL, EDIT_DATE DATE, EDIT_USER VARCHAR2(32 CHAR) ) ON COMMIT DELETE ROWS CACHE;Once again GTT tables are not necessary but I find them very convenient regardless its usage make all a little more complex.
PL/SQL work
Now is time to make some PL/SQL codding which will handle all data in a proper way. Idea for that is taken from Difference between two rows blog post, which I in this way sent many thanks for that work.First we need to create two types, which will be used as result for package functions. o_column_value_difference will hold one record with two values:
- column_value (in mine language-stupac)
- change_pair (in mine language-par_promjene
DROP TYPE O_COLUMN_VALUE_DIFFERENCE FORCE; DROP TYPE C_COLUMN_VALUE_DIFFERENCE FORCE; CREATE OR REPLACE TYPE o_column_value_difference is object ( stupac varchar2(30) ,par_promjene varchar2(4000) ) / CREATE OR REPLACE TYPE c_column_value_difference is table of o_column_value_difference /The main code is bundled in one package COLUMN_DIFF for easier maintainance. Owner of this package must have SELECT privilege on dba_tab_columns view. The best is to place this code to some "special owner" where other dba utilities are set (i.e. TOOLS schema) and grant execute on package to PUBLIC.
Package has only two overloaded functions, which differ in arguments definition, but both have the same meaning-result set. Here is the source code (package declaration):
CREATE OR REPLACE PACKAGE column_diff IS FUNCTION show_column_diffs ( p_owner in varchar2 ,p_table_name in varchar2 ,p_key_value_pair in varchar2 ,p_excluded_columns_list in varchar2 ) RETURN c_column_value_difference; FUNCTION show_column_diffs ( p_owner in varchar2 ,p_table_name in varchar2 ,p_rowid_a in rowid ,p_rowid_b in rowid ,p_excluded_columns_list in varchar2 ) RETURN c_column_value_difference; END column_diff; /First function declaration is based on "p_key_value_pair" parameter which must look like this:
emp_id=1:emp_ver_no=1|emp_id=1:emp_ver_no=2so the routine will change single quotes to double quotes for you and add the WHERE and AND components to finish up a set of where clauses. Please note we are using the pipe (|) and colon (:) as delimiters so if you are using date formats make sure not to use either in your format string it is expected that each expression yeilds one row thus you should be supplying primary or unique key data.
Second function is based on ROWID (do not mix with previously said for ROWID's because this is totally different ROWID) and have for that two parameters:
- p_rowid_a
- p_rowid_b
Here is the code of column_diff package body:
CREATE OR REPLACE PACKAGE BODY column_diff IS FUNCTION show_column_diffs (p_owner IN VARCHAR2, p_table_name IN VARCHAR2, p_rowid_a IN ROWID, p_rowid_b IN ROWID, p_excluded_columns_list IN VARCHAR2) RETURN c_column_value_difference IS BEGIN RETURN show_column_diffs ( p_owner, p_table_name, 'rowid=''' || p_rowid_a || '''|rowid=''' || p_rowid_b || '''', p_excluded_columns_list ); END; -- main func FUNCTION show_column_diffs (p_owner IN VARCHAR2, p_table_name IN VARCHAR2, p_key_value_pair IN VARCHAR2, p_excluded_columns_list IN VARCHAR2) RETURN c_column_value_difference IS where_string_1_v VARCHAR2 (4000); where_string_2_v VARCHAR2 (4000); sql_v VARCHAR2 (32000); col_expression_v VARCHAR2 (32000); c_column_value_difference_v c_column_value_difference := c_column_value_difference (); c_column_value_difference_f_v c_column_value_difference := c_column_value_difference (); BEGIN where_string_1_v := SUBSTR (p_key_value_pair, 1, INSTR (p_key_value_pair, '|') - 1); where_string_1_v := 'a.' || REPLACE (where_string_1_v, ':', ' and a.'); where_string_2_v := SUBSTR (p_key_value_pair, INSTR (p_key_value_pair, '|') + 1); where_string_2_v := 'b.' || REPLACE (where_string_2_v, ':', ' and b.'); FOR i IN 1 .. 100 LOOP sql_v := NULL; FOR r1 IN (SELECT column_name, data_type FROM dba_tab_columns WHERE owner = p_owner AND table_name = p_table_name AND data_type IN ('DATE', 'NUMBER', 'INTEGER', 'PLS_INTEGER', 'VARCHAR2', 'CHAR') AND INSTR ( ',' || UPPER (p_excluded_columns_list) || ',', ',' || column_name || ',' ) = 0 AND column_id BETWEEN (i - 1) * 10 + 1 AND (i * 10) ORDER BY column_name) LOOP IF r1.data_type IN ('DATE', 'NUMBER', 'INTEGER', 'PLS_INTEGER', 'VARCHAR2', 'CHAR') THEN col_expression_v := '''' || r1.column_name || '''' || ' column_name,substr(a.' || LOWER (r1.column_name) || '||''->''||b.' || LOWER (r1.column_name) || ',1,4000) data_value_pair'; ELSE col_expression_v := '''' || r1.column_name || '''' || ' column_name,substr(nvl(to_char(a.' || LOWER (r1.column_name) || ',''dd.mm.yyyy hh24:mi:ss''),lpad('' '',20,'' ''))||''->''||nvl(to_char(b.' || LOWER (r1.column_name) || ',''dd.mm.yyyy hh24:mi:ss''),lpad('' '',20,'' '')),1,4000) data_value_pair'; END IF; -- AUTO_LOG_ERROR_DEBUG ('col_expression_v="'||col_expression_v||'"'); sql_v := sql_v || ' union all select ' || col_expression_v || ' from ' || p_owner || '.' || p_table_name || ' a,' || p_owner || '.' || p_table_name || ' b where ' || where_string_1_v || ' and ' || where_string_2_v || ' and decode(a.' || r1.column_name || ',b.' || r1.column_name || ',0,1) = 1'; END LOOP; -- AUTO_LOG_ERROR_DEBUG ('sql_v="'||sql_v||'"'); IF sql_v IS NOT NULL THEN sql_v := 'select cast(multiset(select * from ( ' || SUBSTR (sql_v, 11) || ' )) as c_column_value_difference ) from dual'; -- AUTO_LOG_ERROR_DEBUG ('prije execute sql_v="'||sql_v||'"'); EXECUTE IMMEDIATE sql_v INTO c_column_value_difference_v; SELECT CAST ( MULTISET ( SELECT * FROM ( SELECT * FROM TABLE ( CAST ( c_column_value_difference_v AS c_column_value_difference) ) UNION ALL SELECT * FROM TABLE ( CAST ( c_column_value_difference_f_v AS c_column_value_difference)))) AS c_column_value_difference ) INTO c_column_value_difference_f_v FROM DUAL; END IF; END LOOP; IF c_column_value_difference_f_v.LAST IS NULL THEN c_column_value_difference_f_v.EXTEND; c_column_value_difference_f_v (c_column_value_difference_f_v.LAST) := o_column_value_difference (NULL, NULL); END IF; RETURN (c_column_value_difference_f_v); END; END column_diff; /The beauty of this solution is dynamic content generation-any table structure is accepted without special knowledge of it before execution happened.
Live demo
Now when all is setup, time is to query history for real. The whole process is processed in two logical steps:- Filling data from EMP_HIST and EMP tables to EMP_HIST_GTT (for one chosen record of EMP table)
- Showing data from EMP_HIST_GTT table with column_diff function
Just to mention that always real data (EMP table) MUST comes before union with HIS table data. Here is the example of step 1:
SQL> INSERT INTO EMP_HIS_GTT ( 2 ID, HIS_ACTION, HIS_DATE, HIS_USER, 3 ENAME, HIREDATE, 4 SAL, CREATE_DATE, CREATE_USER, 5 EDIT_DATE, EDIT_USER 6 ) 7 ( 8 SELECT ID, null, null, null, 9 ENAME, HIREDATE, 10 SAL, CREATE_DATE, CREATE_USER, 11 EDIT_DATE, EDIT_USER 12 FROM EMP 13 WHERE ID = 7369 14 UNION ALL 15 SELECT 16 ID, HIS_ACTION, HIS_DATE, HIS_USER, 17 ENAME, HIREDATE, 18 SAL, CREATE_DATE, CREATE_USER, 19 EDIT_DATE, EDIT_USER 20 FROM EMP_HIS 21 WHERE ID = 7369 AND HIS_ACTION = 'U' 22 ) 23 ; 5 rows created.As espected (1+4 rows) are added to GTT. And now query GTT table with shown function from package. This is that step in action:
SQL> break on id on a_his_date skip 1 SQL> col PAR_PROMJENE for a30 SQL> SQL> SELECT a.id, 2 a_his_date, 3 a_his_user, 4 b.* 5 FROM (SELECT LAG (his_date) OVER (PARTITION BY id ORDER BY his_date) 6 a_his_date, 7 LAG (his_user) OVER (PARTITION BY id ORDER BY his_date) 8 a_his_user, 9 LAG (ROWID) OVER (PARTITION BY id ORDER BY his_date) a_rowid, 10 ROWID b_rowid, 11 EMP_HIS_GTT.* 12 FROM EMP_HIS_GTT 13 WHERE id = 7369 14 ) a, 15 TABLE ( CAST ( 16 tab_apex.column_diff.show_column_diffs ( 17 'TAB_APEX', 18 'EMP_HIS_GTT', 19 a_rowid, 20 b_rowid, 21 'HIS_USER,HIS_ACTION,HIS_DATE,EDIT_USER,EDIT_DATE,CREATE_DATE') AS c_column_value_difference) 22 ) b 23 WHERE NOT A_HIS_DATE IS NULL 24 ORDER BY a_his_date; ID A_HIS_DATE A_HIS_USER STUPAC PAR_PROMJENE ---------- ------------------- -------------------------------- ------------------------------ ------------------------------ 7369 26.01.2012 14:58:32 TAB_APEX SAL 800->900 26.01.2012 15:11:46 TAB_APEX SAL 900->1100 TAB_APEX ENAME SMITH->MR. SMITH 26.01.2012 15:21:10 TAB_APEX ENAME MR. SMITH->SMITH TAB_APEX SAL 1100->1200 SQL>Because first part in select is fixed, small explanation for function call arguments:
- TAB_APEX is user that own EMP_HIS_GTT table.
- a_row_id and a_rowb_id are called from previous subquery-may be fixed in this way
- 'HIS_USER,HIS_ACTION,HIS_DATE,EDIT_USER,EDIT_DATE,CREATE_DATE' is a comma delimited string which holds columns which should not be treated as change
How about delete?
Because trigger will assign "D" in HIS_ACTION column, this will help us distinct that event from other UPDATES.Well if you delete a record, then it doesn't exist in EMP table, so there is no way to join with EMP table. So initialization part (insert records to GTT) should have a little difference look. Here it is code for deleted records history:
SQL> INSERT INTO EMP_HIS_GTT ( 2 ID, HIS_ACTION, HIS_DATE, HIS_USER, 3 ENAME, HIREDATE, 4 SAL, CREATE_DATE, CREATE_USER, 5 EDIT_DATE, EDIT_USER 6 ) 7 ( 8 SELECT 9 ID, HIS_ACTION, HIS_DATE, HIS_USER, 10 ENAME, HIREDATE, 11 SAL, CREATE_DATE, CREATE_USER, 12 EDIT_DATE, EDIT_USER 13 FROM EMP_HIS 14 WHERE ID = 7369 AND HIS_ACTION = 'D' 15 UNION ALL 16 SELECT 17 ID, HIS_ACTION, HIS_DATE, HIS_USER, 18 ENAME, HIREDATE, 19 SAL, CREATE_DATE, CREATE_USER, 20 EDIT_DATE, EDIT_USER 21 FROM EMP_HIS 22 WHERE ID = 7369 AND HIS_ACTION = 'D' 23 UNION ALL 24 SELECT 25 ID, HIS_ACTION, HIS_DATE, HIS_USER, 26 ENAME, HIREDATE, 27 SAL, CREATE_DATE, CREATE_USER, 28 EDIT_DATE, EDIT_USER 29 FROM EMP_HIS 30 WHERE ID = 7369 AND HIS_ACTION = 'U' 31 ) 32 ; 6 rows created. SQL>Here is important that the same query at the beginning is intentionally executed twice and both on EMP_HIS table (instead od EMP as it was for update history query). At the end (as in update part history) updates records from EMP_HIS are added.
Second step is present history in more human way with our package. Changes according browsing history on update part are highlighted.
SQL> SELECT a.id, 2 a_his_action, 3 a_his_date, 4 a_his_user, 5 b.* 6 FROM (SELECT LAG (his_action) OVER (PARTITION BY id ORDER BY his_date) 7 a_his_action, 8 LAG (his_date) OVER (PARTITION BY id ORDER BY his_date) 9 a_his_date, 10 LAG (his_user) OVER (PARTITION BY id ORDER BY his_date) 11 a_his_user, 12 LAG (ROWID) OVER (PARTITION BY id ORDER BY his_date) a_rowid, 13 ROWID b_rowid, 14 EMP_HIS_GTT.* 15 FROM EMP_HIS_GTT 16 WHERE id = 7369 17 ) a, 18 TABLE ( CAST ( 19 tab_apex.column_diff.show_column_diffs ( 20 'TAB_APEX', 21 'EMP_HIS_GTT', 22 a_rowid, 23 b_rowid, 24 'HIS_USER,HIS_ACTION,HIS_DATE,EDIT_USER,EDIT_DATE,CREATE_DATE') AS c_column_value_difference) 25 ) b 26 WHERE NOT A_HIS_DATE IS NULL 27 ORDER BY a_his_date; ID A A_HIS_DATE A_HIS_USER STUPAC PAR_PROMJENE ---------- - ------------------- ------------- ---------- ------------------------------ 7369 U 26.01.2012 14:58:32 TAB_APEX SAL 800->900 U 26.01.2012 15:11:46 TAB_APEX SAL 900->1100 U TAB_APEX ENAME SMITH->MR. SMITH U 26.01.2012 15:21:10 TAB_APEX ENAME MR. SMITH->SMITH U TAB_APEX SAL 1100->1200 D 26.01.2012 22:32:41 TAB_APEX 6 rows selected. SQL>
The End
And if you ask yourself how to know when to show form with history data, answer is easy-it must have DATE_EDIT value not null (in EMP table). If performance allow, add also condition that related child record must exist in EMP_HIST table. Here is small example for that (before 7369 was deleted):SQL> SELECT id, 2 CASE WHEN edit_date IS NULL THEN 'N' 3 ELSE 'Y' 4 END EDITED, 5 ENAME, 6 HIREDATE, 7 SAL, 8 FROM EMP; ID EDITED ENAME HIREDATE SAL ---- ------ ---------- ------------------- ------ 7369 Y SMITH 17.12.1980 00:00:00 1200 7499 N ALLEN 20.02.1981 00:00:00 1600 7521 N WARD 22.02.1981 00:00:00 1250 7566 N JONES 02.04.1981 00:00:00 2975 7654 N MARTIN 28.09.1981 00:00:00 1250 7698 N BLAKE 01.05.1981 00:00:00 2850 7782 N CLARK 09.06.1981 00:00:00 2450 7788 N SCOTT 13.07.1987 00:00:00 3000 7839 N KING 17.11.1981 00:00:00 5000 7844 N TURNER 08.09.1981 00:00:00 1500 7876 N ADAMS 13.07.1987 00:00:00 1100 7900 N JAMES 03.12.1981 00:00:00 950 7902 N FORD 03.12.1981 00:00:00 3000 7934 N MILLER 23.01.1982 00:00:00 1300 14 rows selected. SQL>So creating views with such a derived column is really not to much work.
Hope this helps someone.
Cheers!
P.S.
I'm pretty sure that if you want to stay with standard naming convention, i.e.
No comments :
Post a Comment