13 Ekim 2010 Çarşamba

DBCC Input Buffer ile Tablolarımızı Loglayalım

"DBCC INPUTBUFFER", SQL Server'a client'dan gelen son sql cümleciğini gösterir.

Syntax :
DBCC INPUTBUFFER (session_id [ , request_id ])
[WITH NO_INFOMSGS ]

Parametreleri :
session_id
Son aktif connection'daki oturumun id'sidir.
@@SPID global değişkeni ile, sql server'daki son request'in id si ni alabilirsiniz.

SELECT @@SPID

kodunu çalıştırarak örneğini görebilirsiniz.

request_id

Aktif oturumdaki, son istedğin id sidir.

Son gelen isteğin id sini aşağıdaki kodu kullanarak alabilirsiniz.

SELECT request_id
FROM sys.dm_exec_requests
WHERE session_id = @@spid

Aşağıdaki kod ise, son request ile ilgili daha fazla detay verir. Durumu ne, hangi veri tabanına gelmiş, kim çalıştırmış, ne kadar süre beklemiş gibi.

SELECT *
FROM sys.dm_exec_requests
WHERE session_id = @@spid

WITH

With, diğer DDL komutlarında olduğu gibi bir özellik vereceğimizi ifade eder.

NO_INFOMSGS

Hata seviyesi 0-10 arasında olan yani server tarafında bilgi mesajı olarak kabul edilen seviyede bir mesaj gelirse, bunu gizler.


Bu kadar teorik bilgiden sonra, "DBCC INPUTBUFFER" ın ne işimize yarayacağına ve nasıl kullanılacağımıza bakalım.

CDC kullanmadan, bir tablodaki değişiklikleri log'luyor isek, bu değişikliğin hangi sql cümlesi kullanılarak (Bu bir update/insert/delete statement olabileceği gibi, bir stored procedure de olabilir) yapıldığını saklayabiliriz.

Veya bir tablo, nedenini bulamadığımız bir şekilde güncelleniyor ise, bu güncelleme işleminin hangi kod tarafından yapıldığını yakalayabiliriz.

Aşağıdaki örnek SQL cümlesi, testCustomer isimli bir tablodaki her değişikliği loglayıp, değişiklikleri de testCustomer_Log isimli bir dosya da saklayacağız.


Önce kodlarımızı hazırlayalım.

CREATE TABLE testCustomer(CustomerID int identity(1,1),
                         FirstName varchar(100),
                         LastName varchar(100))
GO

--DROP TABLE testCustomer_Log
CREATE TABLE testCustomer_Log(ID int identity(1,1),
                            CustomerID int,
                            OldFirstName nvarchar(100),
                            OldLastName nvarchar(100),
                            NewFirstName nvarchar(100),
                            NewLastName nvarchar(100),
                            SqlCommand nvarchar(4000),
                            ChangedOn datetime default(GetDate())
                            )
GO

CREATE TRIGGER trTestCustomer ON testCustomer
AFTER UPDATE,INSERT,DELETE
AS BEGIN
  DECLARE @InputBuffer TABLE(EventType nvarchar(30),
                             Parameters int
                             EventInfo nvarchar(4000))

  INSERT INTO @InputBuffer
   exec ('DBCC INPUTBUFFER(@@SPID) WITH NO_INFOMSGS')

  DECLARE @SQLCommand nvarchar(4000)
  SELECT @SQLCommand = EventInfo
  FROM @InputBuffer

  INSERT INTO testCustomer_Log(CustomerID,OldFirstName,OldLastName,
                               NewFirstName,NewLastName,SqlCommand)
  SELECT isNull(i.CustomerID,d.CustomerID),d.FirstName,d.LastName,
        i.FirstName,i.LastName,@SQLCommand
  FROM inserted i
  FULL JOIN deleted d on d.CustomerID = i.CustomerID

END

GO
CREATE PROCEDURE spTEST_UpdateCustomerName @FirstName nvarchar(100),@CustomerID int
AS BEGIN
  UPDATE testCustomer SET FirstName = @FirstName WHERE CustomerID = @CustomerID
END

Data ve Log tablolarını yarattıktan sonra, Data tablomuz olan testCustomer üzerine bir trigger yazıyoruz. Bu trigger de @@SPID ile son oturumun id sini alıp, DBCC INPUTBUFFER a veriyoruz. Bize uyarı mesajları vermesin diye de mesajları "WITH NO_INFOMSG" yazarak kapatıyoruz. 

DBCC INPUTBUFFER 3 kolondan, 1 satırdan oluşan bir tablo döndürür. Bu tablodan dönen değerleri @InputBuffer isimli bir değişken tabloya yazıyoruz ki, sonra DML kodunu çalıştıran kodu alabilelim. 
EventInfo kolonunda, nvarchar(4000) tipinde bir bilgi gelir.  İşte datayı değiştiren kod da bu kolonda bulunur. Yani çalışan script eğer 4000 karakterden fazla ise, ilk 4000 karakterini görebiliriz. Bu kodu @SQLCommand değişkenine atıyoruz.

Sonra değişen bilgilerin eski ve yeni bilgilerini yazabilmek için de, trigger içerisinde kullanılan inserted ve deleted tablolarını full join ile bağlıyoruz.
Unutmamak gerekir ki, bir tabloya insert kodu ile bir kayıt ekliyorsak, sadece inserted tablosunda kayıtlar olur, deleted tablosundan birşey gelmez. Aynı mantıkla, delete ile kayıt siliyorsak, sadece deleted tablosunda silinen kaydın bilgisi olur, inserted tablosunda birşey olmaz. Update ile tabloyu güncelliyor isek, yeni bilgiler inserted tablosunda, eski bilgiler de deleted tablosunda bulunur. 


Birkaç örnek ile trigger ın log tablosuna neler yazdığını bulalım.


INSERT INTO testCustomer(FirstName,LastName) VALUES('Abc','DEF')
INSERT INTO testCustomer(FirstName,LastName) VALUES('Abc','fsfdsfdsf')
GO
INSERT INTO testCustomer(FirstName,LastName) VALUES('TestFN','TestLN')
GO
INSERT INTO testCustomer(FirstName,LastName) VALUES('TestFN2','TestLN2')
GO


UPDATE testCustomer SET FirstName = 'Abc Updated' WHERE FirstName = 'Abc'
GO
DELETE FROM testCustomer WHERE FirstName='TestFN'
GO
DECLARE @CustomerID int

SELECT @CustomerID = CustomerID
FROM testCustomer
WHERE FirstName='TestFN2'

exec spTEST_UpdateCustomerName @CustomerID = @CustomerID, @FirstName = 'TestFN3'

GO
SELECT * FROM testCustomer
SELECT * FROM testCustomer_Log


İşte Customer tablosunun son hali. 4 kayıt ekledik.  Kayıtlardan birini update cümlesi ile güncelledik. Sonra başka bir kayıt sildik. Daha sonra da adı TestFN2 olan kaydın adını yazdığımız stored procedure ile güncelledik. 

CustomerID FirstName LastName
1 Abc Updated DEF
2 Abc Updated fsfdsfdsf
4 TestFN3 TestLN2


 Acaba log tablomuzda neler olmuş?





ID CustomerID OldFirstName OldLastName NewFirstName NewLastName SqlCommand ChangedOn
1 1

Abc DEF INSERT INTO testCustomer(FirstName,LastName) VALUES('Abc','DEF')

INSERT INTO testCustomer(FirstName,LastName) VALUES('Abc','fsfdsfdsf')
13.10.2010
2 2

Abc fsfdsfdsf INSERT INTO testCustomer(FirstName,LastName) VALUES('Abc','DEF')

INSERT INTO testCustomer(FirstName,LastName) VALUES('Abc','fsfdsfdsf')
13.10.2010
3 3

TestFN TestLN INSERT INTO testCustomer(FirstName,LastName) VALUES('TestFN','TestLN') 13.10.2010
4 4

TestFN2 TestLN2 INSERT INTO testCustomer(FirstName,LastName) VALUES('TestFN2','TestLN2') 13.10.2010
5 2 Abc fsfdsfdsf Abc Updated fsfdsfdsf



UPDATE testCustomer SET FirstName = 'Abc Updated' WHERE FirstName = 'Abc'
13.10.2010
6 1 Abc DEF Abc Updated DEF



UPDATE testCustomer SET FirstName = 'Abc Updated' WHERE FirstName = 'Abc'
13.10.2010
7 3 TestFN TestLN

DELETE FROM testCustomer WHERE FirstName='TestFN' 13.10.2010
8 4 TestFN2 TestLN2 TestFN3 TestLN2 DECLARE @CustomerID int



SELECT @CustomerID = CustomerID

FROM testCustomer

WHERE FirstName='TestFN2'



exec spTEST_UpdateCustomerName @CustomerID = @CustomerID, @FirstName = 'TestFN3'
13.10.2010


İşte tüm yaptığımız değişikliker burada görünüyor.
İlk başta çalıştırdığımız insert cümlesi 2 kayıt birden insert edecek bir batch şeklinde yazılmıştı.  Customer 1 ve Customer 2 kayıtlarında bu her iki cümlenin de yazdığını görebiliyoruz.
En ilginci de, 4 nolu CustomerID nin update cümlesi. Burada ilgili müşteri numarasını bulduktan sonra bunu bir procedure e veriyor ve onun güncelleme yapmasını bekliyorduk.  Yazdığımız tüm kod da burada aynen görünebiliyor.


 Peki testCustomer tablosunu TRUNCATE TABLE ile temizlersek ne olur? Log tablosuna hiç bir şey gelmez. Çünkü truncate kodu herhangi bir trigger'ı tetiklemez. Biz de işlerimizi trigger ile yaptığımız için log tablosunun herhangi birşeyden haberi olmaz.  




Bir sonraki post'da DBCC INPUTBUFFER a benzer iş yapan fn_get_sql sistem fonksiyonundan ve ikisi arasındaki farklardan bahsedeceğiz.