I came across a question on ServerFault this afternoon that inflamed my desire to be ultra-geeky (it was really already inflamed after teaching backup and restore internals all afternoon). Basically the question boiled down to how to find out who dropped a table if there’s no other way except the transaction log (e.g. no tracing is available, even the default trace). So I hacked around and figured out at least how to find out *when* a particular table was dropped plus the UID and SPID of who dropped it.
Everything hinges on using undocumented commands to look into the transaction log. I’ve played with this before on the blog: fn_dblog.
First off I created a script to create a database, populate a table and then drop it.
USE master;
GO
CREATE DATABASE FnDbLogTest;
GO
USE FnDbLogTest;
GOCREATE TABLE TestTable (
c1 INT IDENTITY,
c2 CHAR (100) DEFAULT ‘a’);
GO
CREATE CLUSTERED INDEX TT_CL ON TestTable (c1);
GO
INSERT INTO TestTable DEFAULT VALUES;
GOSELECT OBJECT_ID (‘TestTable’);
GODROP TABLE TestTable;
GO
First we need to find the transactions that drop tables in the log. The following code works for 2000, 2005, and 2008:
SELECT [Transaction Id], [Begin Time], [UID], [SPID]
FROM ::fn_dblog (NULL, NULL)
WHERE [Transaction Name] = ‘DROPOBJ’
GO
The (NULL, NULL) is the starting LSN and ending LSN to process – NULL means process everything available.
Results on SQL Server 2000 will look as follows for me logging in as a Windows login in the SA role:
Transaction Id Begin Time UID SPID
————– ———————– ———– ———–
0000:000000e0 2009/06/16 18:23:03:320 1 51
And for the same circumstances on 2005 and 2008, the results look like:
Transaction Id Begin Time UID SPID
————– ———————— ———– ———–
0000:00000587 2009/06/16 17:49:56:927 -1 51
If the user who dropped the table logged in as a role member, and they’re not connected as that SPID any more, you may not be able to tell who it was unless you’re also tracking successful logins into your server – but you’ll at least know what role it was.
Now, this only shows us that a table was dropped, not which table it was. There’s no way to get the name of the table that was dropped, only the object ID – so you’ll need to have some other way to determine what the table ID is if there are multiple table drops and only one of them is malignant.
For SQL Server 2000, the code to find which object ID we’re talking about is as follows (dropping the Transacation Id into the WHERE clause):
SELECT DISTINCT [Object Name] FROM ::fn_dblog (NULL, NULL)
WHERE [Transaction Id] = ’0000:000000e0′
AND [Context] = ‘LCX_IAM’;
GOObject Name
——————–
(2009058193)
The object ID in parentheses is the ID of the table that was dropped.
For SQL Server 2005 and 2008, the code is as follows (with the same Transaction Id substitution):
SELECT TOP (1) [Lock Information] FROM ::fn_dblog (NULL, NULL)
WHERE [Transaction Id] = ’0000:00000587′
AND [Lock Information] LIKE ‘%SCH_M OBJECT%’;
GOLock Information
——————————————–
ACQUIRE_LOCK_SCH_M OBJECT: 8:2073058421:0
The 8:2073058421 is the database ID and object ID of the table that was dropped.
Now you can go find whoever it was and take whatever action you deem appropriate :-)
Hope this helps!
PS If you find the you don’t get enough info from ::fn_dblog, try turning on trace flag 2537. It allows the function to look at *all* possible log, not just the active log.
14 Responses to Finding out who dropped a table using the transaction log
Hi Paul
I think we are able to get such info in SQL Server 2005 onwards by using Database Triggers, what do you think?
Very nice. Paul this is very complex subject explained in easy manner.
Kind Regards,
Pinal
Nice article Paul
As per the comment that’s true if you setup a DDL Trigger for auditing pro-actively. What Paul’s showing is how to get the info after the fact. If you want an example of a ddl trigger – check out this post: http://www.sqlskills.com/blogs/kimberly/post/EXECUTE-AS-and-an-important-update-your-DDL-Triggers-(for-auditing-or-prevention).aspx
Cheers,
kt
And of course the option to use the default trace which, if not already overwritten or restarted captures the relevant data.
-Jens
Awesome Dude! So happy to see you discuss this too!!! Back in phracking cold January, I dug into this topic also:
http://www.sqlservercentral.com/blogs/hugo/archive/2009/01/17/it-s-minus-twenty-five-outside-and-i-m-writing-about-transaction-log-files.aspx
I’ve added an alert script for those of you using Database mail, furthermore, I did a dumbed-down demo just like this last week in at SQLteach in Vancouver. Great topic for the extra geeky DBAs :)
::fn_dblog.AllocUnitName column is usuall the object touched upon, but I’ll dig into this over the coming week, interesting note about the role issue, thanks a whole bunch.
It seems this doesn’t work in SQL Server 2008. I tried 10.0.2531 and 10.0.1600, both same error.
Msg 207, Level 16, State 1, Line 1
Invalid column name ‘UID’.
Paul
Thanks for great article.
As someone mentioned above in 2008 no UID field ..
However i am able to co-relate with Transaction SID with my NT account even tho i am in my global DBA group(dropped the table).
I am quite sure that Transaction SID is tied with my Windows security ID . This information not aviable in SQL SERVER( may be in Active Directory)
"Microsoft Windows security ID that is associated with the original_login_name. Is not nullable" (sys.dm_exec_session)
SELECT [Transaction Id], [Begin Time], [SPID] ,[TRansaction SID]
FROM ::fn_dblog (NULL, NULL)
WHERE [Transaction Name] = ‘DROPOBJ’
[...] Finding out who dropped a table using the transaction log [...]
[...] A SQL Server DBA myth a day: (19/30) TRUNCATE TABLE is non-logged Benchmarking: 1-TB table population (part 2: optimizing log block IO size and how log IO works) Lock logging and fast recovery How do checkpoints work and what gets logged Finding out who dropped a table using the transaction log [...]
I just wanted to point out to a fact that an object deletion can happen as part of a transaction. In this case the investigation becomes a tad more tedious. In short, the DROPOBJ transaction will not be there. There will be ‘user_transaction’ instead. Also, in my case the context was LCX_NULL, and operation LOP_BEGIN_XACT. In the lock information field I got HoBt 0:ACQUIRE_LOCK_SCH_M METADATA: database_id = 5 SEQUENCE($seq_type = 0, object_id = 523148909). That object ID was the one deleted.
Good point!
Forgot to added that once you have that long number either from the [Description] or [TRansaction SID] column e.g. user_transaction;0x010500000000000515000000b0e022bfe57461a9e3436aa95d0e0000 you can use function SUSER_SNAME to return the actual user identity (or domain account) as
SELECT SUSER_SNAME(0x010500000000000515000000B0E022BFE57461A9E3436AA95D0E0000);
Result: DomainName\FirstName.LastName