FLASHBACK TABLE vs. DBA_OBJECTS . LAST_DDL_TIME
NOTE: This post originally appeared on The Pythian Group blog on 6 June 2008, and is reposted here with permission.
A little over a week ago, a teammate and I were trying to use Oracle’s FLASHBACK TABLE to undo an “oops” UPDATE statement that a client’s developers had run on one of their test databases, clearing data from two columns in all rows of the table. The statement was actually part of a script that also contained ALTER TABLE statements to add columns. This is important to note because FLASHBACK TABLE will only let you go back as far as the most recent DDL against that table. To quote the SQL reference, “Oracle Database cannot restore a table to an earlier state across any DDL operations that change the structure of the table.”
This led me to another question: Is there a way to directly see to precisely what date and time you can flashback a table? The developer couldn’t give me a precise time, only that the UPDATE statement was executed immediately after the structure-changing DDL, making my target window very small. Naturally, one would think that the LAST_DDL_TIME in the DBA_OBJECTS view would hit that nail on the head. However it turns out that the key bit of that SQL reference quote is “change the structure of the table.”
It turns out that there are a few statements that will update the LAST_DDL_TIME without changing the table structure. For example, GRANT and REVOKE statements, which provide a user with certain privileges on an object, will trigger an update to LAST_DDL_TIME. You can then go ahead and flashback the table prior to the privilege change. Another item to note is that a prerequisite to FLASHBACK TABLE is to enable row movement on that table, via (you guessed it) an ALTER TABLE statement. The ALTER TABLE foo ENABLE ROW MOVEMENT statement also bumps LAST_DDL_TIME, but obviously doesn’t block FLASHBACK TABLE from going past it in time.
The bottom of all this is that you can’t use LAST_DDL_TIME to determine just how far back you can go with a FLASHBACK TABLE statement, as you can most likely go past it due to various non-structure-changing DDL statements that affect that timestamp.
Here’s a little demonstration to illustrate this point:
-- Get our preferred date format
SQL> alter session set nls_date_format='YYYY/MM/DD HH24:MI:SS';
Session altered.
SQL>
SQL> -- First we need something to play with
SQL> create table emp
2 as select * from hr.employees;
Table created.
SQL>
SQL> -- alter row movement to allow for flashback
SQL> -- NOTE: this also updates last_ddl_time
SQL> alter table emp enable row movement;
Table altered.
SQL>
SQL> -- Note the last_ddl_time given in the
SQL> -- dba_objects view
SQL> select last_ddl_time
2 from dba_objects
3 where owner=user and object_name='EMP';
LAST_DDL_TIME
-------------------
2008/05/28 22:09:11
SQL>
SQL> -- Let some time elapse, get a drink of water
SQL> exec dbms_lock.sleep(120);
PL/SQL procedure successfully completed.
SQL>
SQL> -- Get a marker, note this.
SQL> select sysdate from dual;
SYSDATE
-------------------
2008/05/28 22:11:11
SQL>
SQL> -- Note the salary before the DML
SQL> select first_name, last_name, salary
2 from emp
3 where employee_id=101;
FIRST_NAME LAST_NAME SALARY
-------------------- ------------------------- ----------
Neena Kochhar 17000
SQL>
SQL> -- Let's give her a 15% raise
SQL> update emp
2 set salary = salary * 1.15
3 where employee_id=101;
1 row updated.
SQL>
SQL> -- Don't forget the commit!
SQL> commit;
Commit complete.
SQL>
SQL> -- Observe the new salary
SQL> select first_name, last_name, salary
2 from emp
3 where employee_id=101;
FIRST_NAME LAST_NAME SALARY
-------------------- ------------------------- ----------
Neena Kochhar 19550
SQL>
SQL> -- Get another marker
SQL> select sysdate from dual;
SYSDATE
-------------------
2008/05/28 22:11:11
SQL>
SQL> -- Now let's run bump last_ddl_time with a grant
SQL> grant select on emp to hr;
Grant succeeded.
SQL>
SQL> -- Check it out
SQL> select last_ddl_time
2 from dba_objects
3 where owner=user and object_name='EMP';
LAST_DDL_TIME
-------------------
2008/05/28 22:11:11
SQL> -- Now flashback to the first marker or
SQL> -- anytime after creation but before update
SQL> flashback table emp to timestamp
2 to_timestamp('2008/05/28 22:10:00','YYYY/MM/DD HH24:MI:SS');
Flashback complete.
SQL>
SQL> -- Verify the flashback worked
SQL> select first_name, last_name, salary
2 from emp
3 where employee_id=101;
FIRST_NAME LAST_NAME SALARY
-------------------- ------------------------- ----------
Neena Kochhar 17000
SQL>
SQL> -- Clean up
SQL> drop table emp;
Table dropped.
SQL>
So, I’m still looking for a straight-forward way to identify a point-of-no-return for flashback for future reference. When I asked around the shop, the consensus seemed to be that using LogMiner would be the best way to not only find the SCN of the table structure changes, but to get the SQL to “undo” the table change (if I so desired), and even to undo the effects of the UPDATE statement.
Apologies for the long block of code. And thanks to Marc, Riyaj, Alex F., and Alex G. for their help.




Don,
Interesting post.
Have you tried SPARE6 column from SYS.TAB$ table?
I think it may give you the answer you are looking for.
I did several tests and it works well (tried with grant/revoke privileges, enable row movement, change table structure).
Example:
SQL> spool test_last_ddl.log
SQL> create table test_tab
2 as select * from all_objects where 1=2;
Table created.
1 select a.object_name, a.object_id,
2 to_char(a.last_ddl_time,’MM/DD/YYYY HH24:MI:SS’) last_ddl_time,
3 to_char(b.spare6,’MM/DD/YYYY HH24:MI:SS’) spare6
4 from dba_objects a,
5 sys.tab$ b
6 where a.object_name=’TEST_TAB’
7* and a.object_id=b.obj#
SQL> /
OBJECT_NAM OBJECT_ID LAST_DDL_TIME SPARE6
———- ———- ——————- ——————-
TEST_TAB 140194 06/30/2008 14:50:18 06/30/2008 14:50:18
1* grant select on test_tab to scott
SQL> ed
Wrote file afiedt.buf
1 select a.object_name, a.object_id,
2 to_char(a.last_ddl_time,’MM/DD/YYYY HH24:MI:SS’) last_ddl_time,
3 to_char(b.spare6,’MM/DD/YYYY HH24:MI:SS’) spare6
4 from dba_objects a,
5 sys.tab$ b
6 where a.object_name=’TEST_TAB’
7* and a.object_id=b.obj#
SQL> /
OBJECT_NAM OBJECT_ID LAST_DDL_TIME SPARE6
———- ———- ——————- ——————-
TEST_TAB 140194 06/30/2008 14:51:43 06/30/2008 14:50:18
– last_ddl_time changed its value, but SPARE6 didn’t
SQL> revoke select on test_tab from scott;
Revoke succeeded.
SQL> select a.object_name, a.object_id,
2 to_char(a.last_ddl_time,’MM/DD/YYYY HH24:MI:SS’) last_ddl_time,
3 to_char(b.spare6,’MM/DD/YYYY HH24:MI:SS’) spare6
4 from dba_objects a,
5 sys.tab$ b
6 where a.object_name=’TEST_TAB’
7 and a.object_id=b.obj#;
OBJECT_NAM OBJECT_ID LAST_DDL_TIME SPARE6
———- ———- ——————- ——————-
TEST_TAB 140194 06/30/2008 14:52:34 06/30/2008 14:50:18
– the same behavior
SQL> alter table test_tab enable row movement;
Table altered.
SQL> select a.object_name, a.object_id,
2 to_char(a.last_ddl_time,’MM/DD/YYYY HH24:MI:SS’) last_ddl_time,
3 to_char(b.spare6,’MM/DD/YYYY HH24:MI:SS’) spare6
4 from dba_objects a,
5 sys.tab$ b
6 where a.object_name=’TEST_TAB’
7 and a.object_id=b.obj#;
OBJECT_NAM OBJECT_ID LAST_DDL_TIME SPARE6
———- ———- ——————- ——————-
TEST_TAB 140194 06/30/2008 14:53:04 06/30/2008 14:50:18
– same thing
SQL> desc test_tab
Name Null? Type
—————————————– ——– —————————-
OWNER NOT NULL VARCHAR2(30)
OBJECT_NAME NOT NULL VARCHAR2(30)
SUBOBJECT_NAME VARCHAR2(30)
OBJECT_ID NOT NULL NUMBER
DATA_OBJECT_ID NUMBER
OBJECT_TYPE VARCHAR2(19)
CREATED NOT NULL DATE
LAST_DDL_TIME NOT NULL DATE
TIMESTAMP VARCHAR2(19)
STATUS VARCHAR2(7)
TEMPORARY VARCHAR2(1)
GENERATED VARCHAR2(1)
SECONDARY VARCHAR2(1)
– now lets see what happens after a change in table structure
SQL> alter table test_tab
2 modify secondary varchar2(2);
Table altered.
SQL> select a.object_name, a.object_id,
2 to_char(a.last_ddl_time,’MM/DD/YYYY HH24:MI:SS’) last_ddl_time,
3 to_char(b.spare6,’MM/DD/YYYY HH24:MI:SS’) spare6
4 from dba_objects a,
5 sys.tab$ b
6 where a.object_name=’TEST_TAB’
7 and a.object_id=b.obj#;
OBJECT_NAM OBJECT_ID LAST_DDL_TIME SPARE6
———- ———- ——————- ——————-
TEST_TAB 140194 06/30/2008 14:53:48 06/30/2008 14:53:48
– both columns changed its value
SQL>
It seems like SPARE6 contains the time of the last structure change, unless you disprove this assumption with a counter example.
Cheers,
Mihajlo
Mihajlo Tekic
June 30, 2008 at 2:13 pm
Have you tried a running a trace when you do a FLASHBACK TABLE ? The recursive SQL may indicate where it is looking.
Gary
June 30, 2008 at 5:02 pm