Montag , 14 Oktober 2019

SQL Server Closeup #6: Ghost Data Records unter der Lupe

Nachdem in den letzten Postings der "SQL Server Closeup" Serie viel Theorie an der Reihe war, werden wir heute die Ghost Data Records einmal "Live" unter die Lupe nehmen.

Hierzu benötigen wir natürlich als erstes eine Datenbank und eine Tabelle.
Das Skript sieht hierfür wie folgt aus:

USE [master]
GO
IF DATABASEPROPERTY('SQLServerCloseup', 'Version') > 0 BEGIN ALTER DATABASE [SQLServerCloseup] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [SQLServerCloseup];
END;
GO CREATE DATABASE SQLServerCloseup;
GO USE [SQLServerCloseup];
GO CREATE TABLE [dbo].[Produkte]
(
    [ProduktID] [int] NOT NULL,
    [ProduktBezeichnung] [varchar](50) NOT NULL,
    CONSTRAINT [PK_Produkte] PRIMARY KEY CLUSTERED 
 (
        [ProduktID] ASC
 ) )

Nachdem nun eine Tabelle zum Testen existiert, können Datensätze hinzugefügt und gelöscht werden.
Damit das Ganze besser nachvollziehbar ist, verwende ich eine benannte Transaktion, die ich vorerst offen lasse:

BEGIN TRANSACTION GhostRecordTest;

INSERT INTO [dbo].[Produkte]
VALUES (1, 'Bleistift'), (2, 'Kugelschreiber'), (3, 'Buntstift');
       
DELETE FROM [dbo].[Produkte] 
WHERE ProduktID = 2;

Um die File ID und Page in File ID zu ermitteln, verwende ich den SQL Befehl DBCC IND [Mehr]:

DBCC IND ('SQLServerCloseup', 'Produkte', 1);

DBCC IND Ausgabe

Danach kann der Inhalt der Page mittels DBCC PAGE [Mehr] angezeigt werden:

DBCC PAGE ('SQLServerCloseup', 1, 78, 1);

Die wichtigen Stellen der Ausgabe ich ich mit rot hervorgehoben:

PAGE: (1:78)

...Ausgabe an dieser Stelle gekürzt...

PAGE HEADER:

Page @0x12E34000

m_pageId = (1:78)               m_headerVersion = 1             m_type = 1
m_typeFlagBits = 0x4            m_level = 0                     m_flagBits = 0x0
...Ausgabe an dieser Stelle gekürzt...
m_xactReserved = 0              m_xdesId = (0:597)              m_ghostRecCnt = 1
m_tornBits = 0
...Ausgabe an dieser Stelle gekürzt...

DATA:

Slot 0, Offset 0x60, Length 24, DumpStyle BYTE

Record Type = PRIMARY_RECORD      Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 24
Memory Dump @0x6378C060

00000000:   30000800 01000000 02000001 00180042 †0..............B
00000010:   6c656973 74696674 †††††††††††††††††††leistift

Slot 1, Offset 0x78, Length 29, DumpStyle BYTE

Record Type = GHOST_DATA_RECORD   Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 29
Memory Dump @0x6378C078

00000000:   3c000800 02000000 02000001 001d004b †<..............K
00000010:   7567656c 73636872 65696265 72††††††††ugelschreiber

Slot 2, Offset 0x95, Length 24, DumpStyle BYTE

Record Type = PRIMARY_RECORD      Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 24
Memory Dump @0x6378C095

00000000:   30000800 03000000 02000001 00180042 †0..............B
00000010:   756e7473 74696674 †††††††††††††††††††untstift

OFFSET TABLE:

Row - Offset
2 (0x2) - 149 (0x95)
1 (0x1) - 120 (0x78)
0 (0x0) - 96 (0x60)

Wie man sehen kann, wurde der zweite Record als Ghost Data Record markiert, sowie im Page Header der Ghost Record Count um 1 erhöht.

Mit der undokumentierten Funktion fn_dblog kann man dies auch im Transaction Log der Datenbank nachvollziehen:

DECLARE @TransactionID NVARCHAR(MAX) 

SELECT @TransactionID = [Transaction ID] 
FROM   fn_dblog(null, null)
WHERE  [Transaction Name] = 'GhostRecordTest'; 

SELECT *
FROM   fn_dblog(null, null) 
WHERE  [Transaction ID] = @TransactionID;

Transaction Log Einträge der aktuellen Transaktion

Außerhalb der aktuellen Transaktion kann man bereits die Anpassungen der zugehörigen PFS Page erkennen:

Transaction Log Einträge für die PFS Page

Um besser erkennen zu können, was nach dem COMMIT der Transaktion passiert, muss als nächstes die letzte LSN bestimmt werden:

SELECT MAX([Current LSN]) FROM fn_dblog(null, null);

Anschließend kann die Transaktion beendet und direkt die weiteren Transaction Log Einträge angezeigt werden:

COMMIT TRANSACTION; SELECT [Page ID], * 
FROM fn_dblog(null, null) 
WHERE [Current LSN] > '00000018:00000052:001a';

Ein wenig später ist auch der Ghost Cleanup Task [Mehr – siehe Ghost Records] gelaufen.

Wenn man sich die Page jetzt erneut anzeigen lässt, kann man erkennen, dass nur im Row Offset Array der Record Zeiger entfernt wurde:

DBCC PAGE ('SQLServerCloseup', 1, 78, 2);

Ausgabe von DBCC PAGE:

PAGE: (1:78)

...Ausgabe an dieser Stelle gekürzt...

PAGE HEADER:

Page @0x130FC000

m_pageId = (1:78)               m_headerVersion = 1             m_type = 1
m_typeFlagBits = 0x4            m_level = 0                     m_flagBits = 0x0

...Ausgabe an dieser Stelle gekürzt...

m_xactReserved = 0              m_xdesId = (0:597)              m_ghostRecCnt = 0
m_tornBits = 0

...Ausgabe an dieser Stelle gekürzt...

DATA:

Memory Dump @0x617BC000

617BC000:   01010400 00000001 00000000 00000800 †................
617BC010:   00000000 00000200 1d000000 6c1fad00 †............l.­.
617BC020:   4e000000 01000000 18000000 57000000 †N...........W...
617BC030:   01000000 55020000 00000000 00000000 †....U...........
617BC040:   01000000 00000000 00000000 00000000 †................
617BC050:   00000000 00000000 00000000 00000000 †................
617BC060:   30000800 01000000 02000001 00180042 †0..............B
617BC070:   6c656973 74696674 3c000800 02000000 †leistift<.......
617BC080:   02000001 001d004b 7567656c 73636872 †.......Kugelschr
617BC090:   65696265 72300008 00030000 00020000 †eiber0..........
617BC0A0:   01001800 42756e74 73746966 74000021 †....Buntstift..!
617BC0B0:   21212121 21212121 21212121 21212121 †!!!!!!!!!!!!!!!!
617BC0C0:   21212121 21212121 21212121 21212121 †!!!!!!!!!!!!!!!!
617BC0D0:   21212121 21212121 21212121 21212121 †!!!!!!!!!!!!!!!!
...Ausgabe an dieser Stelle gekürzt...
617BDFE0:   21212121 21212121 21212121 21212121 †!!!!!!!!!!!!!!!!
617BDFF0:   21212121 21212121 21219500 95006000 †!!!!!!!!!!?.?.`.

OFFSET TABLE:

Row - Offset
1 (0x1) - 149 (0x95)
0 (0x0) - 96 (0x60)

Verwendete Quellen: SQLskills.com und Microsoft® SQL Server® 2008 Internals (ISBN-10: 0735626243)