Mittwoch, 21. August 2013

Interne Verwaltung von Forwarded Records

Häufig lese ich oder höre in Gesprächen, dass FORWARDED RECORDS auf jeder Datenseite, auf der sie einmal gespeichert wurden, weitere Pointer zum neuen Speicherort hinterlassen. Diese Aussage ist nicht zutreffend. Änderungen werden nur auf der Datenseite hinterlegt, auf der ein Datensatz initial gespeichert wird. Dieser Artikel zeigt die Zusammenhänge zwischen HEAP und FORWARDED RECORDS

Was ist ein FORWARDED RECORD

FORWARDED RECORDS können nur in HEAPS auftreten. Von einem FORWARDED RECORD spricht man, wenn ein Datensatz bei einer Aktualisierung nicht mehr vollständig auf der Datenseite gespeichert werden kann. Von einem HEAP spricht man, wenn eine Relation keinen Clustered Index besitzt. Datensätze werden in einem HEAP beim Speichern auf einer Datenseite unmittelbar hintereinander abgelegt während in einem Clustered Index immer eine logische Sortierung nach dem Clustered Key erforderlich ist.
Die Offsets für den Beginn eines neuen Datensatzes werden in sogenannten Slots gespeichert. Insgesamt stehen einer Datenseite (8.192 Bytes) für Daten 8.060 Bytes und für Slots 36 Bytes zur Verfügung. 96 Bytes entfallen auf den Page Header.
PAGE_STRUCTURE_01

Wann tritt ein FORWARDED RECORD auf?

FORWARDED RECORDS können nur in Relationen mit Attributen variabler Datenlänge auftreten, in denen Aktualisierungen in den Attributen mit variabler Datenlänge durchgeführt werden. Besitzt eine Relation ausschließlich Attribute mit festen Datenlängen (char, nchar, …), können keine FORWARDED RECORDS auftreten, da bereits beim Einfügen Daten die vordefinierte Datenlänge vollständige für den Eintrag reserviert wird. Ist der einzutragende Wert kürzer als die Länge des Attributs, wird der nicht verwendete Bereich mit Leerzeichen (0x20) aufgefüllt (gilt nicht für numerische Datentypen).
PAGE_STRUCTURE_02
Die obige Abbildung zeigt eine Datenseite mit insgesamt 5 Datensätzen; der Bereich für Daten ist fast vollständig aufgefüllt. Wird Datensatz 1 aktualisiert und der Datensatz kann nicht mehr vollständig auf der Datenseite gespeichert werden, muss der Datensatz auf eine neue Datenseite verschoben werden, auf der dieser Datensatz abgespeichert werden kann. Beim “Verlassen” des ursprünglichen Speicherorts wird die neue Adresse (ähnlich einem Nachsendeantrag) auf der ursprünglichen Datenseite hinterlassen.
PAGE_STRUCTURE_03
Die Abbildung zeigt, wie ein FORWARDED RECORD intern verwaltet wird. Nach der Aktualisierung passt der Datensatz nicht mehr in den ihm ursprünglich zugewiesenen Datenbereich; unmittelbar im Anschluss wird der Bereich bereits durch Datensatz 2 allokiert. Da der Datensatz weder in den ursprünglichen Bereich passt noch an das Ende der Datenseite verlagert werden kann, muss der Datensatz auf die nächste freie Datenseite verschoben werden, auf der noch ausreichend Platz zur Verfügung steht, um den Datensatz abzuspeichern. Sobald der Datensatz auf der Seite abgespeichert wurde, wird der ursprüngliche Datenbereich aktualisiert, indem der vormals vom Datensatz allokierte Bereich auf 11 Bytes reduziert wird. Diese 11 Bytes speichern den Typen des Datensatzes sowie die neue Adresse, unter der der Datensatz abgerufen werden kann. Genau an diesem Punkt hat sich immer wieder das Gerücht etabliert, dass Microsoft SQL Server beim erneuten “Umzug” des Datensatzes die neue Adresse auf der “letzten bekannten” Adresse hinterlegt. Wird der [Datensatz 1], der sich nun auf Datenseite 2 befindet, erneut aktualisiert, wird die neue Adresse nicht mehr auf Datenseite 1 gespeichert sondern auf Datenseite 2 – es würde also eine Adresskette entstehen. Das ist FALSCH!

Testumgebung

Um alle Aspekte eines FORWARDED RECORD zu berücksichtigen, wird eine Relation mit 3 Attributen erzeugt. Für das Attribut [col1] wird zu Demonstrationszwecken eine feste Datenlänge gewählt wird während das Attribut [col2] eine variable Datenlänge besitzt.
   1: IF OBJECT_ID('dbo.tbl_Heap', 'U') IS NOT NULL

   2:     DROP TABLE dbo.tbl_Heap

   3:     GO

   4:  

   5: CREATE TABLE dbo.tbl_Heap

   6: (

   7:     Id      int              NOT NULL    IDENTITY(1, 1),

   8:     col1    char(500)        NOT NULL,

   9:     col2    varchar(8000)    NOT NULL

  10: );

Nachdem die Relation fertig gestellt ist, kann sie mit Beispieldaten gefüllt werden. Es werden insgesamt 50 Datensätze in die Relation eingetragen.

 

   1: -- Datensätze hinzufügen

   2: SET NOCOUNT ON

   3: GO

   4:  

   5: DECLARE    @i int = 1;

   6: WHILE @i <= 50

   7: BEGIN

   8:     INSERT INTO dbo.tbl_Heap (col1, col2)

   9:     SELECT  REPLICATE ('A', @i),

  10:             REPLICATE ('B', @i);

  11:  

  12:     SET    @i += 1;

  13: END

  14: GO

  15:  

  16: -- Ausgabe der Daten mit Informationen über die Allocation

  17: SELECT sys.fn_PhysLocFormatter(%%physloc%%) AS Location, * FROM dbo.tbl_Heap;

  18: GO


Die Funktion sys.fn_PhysLocFormatter zeigt die logische Zuordnung eines Datensatzes auf einer Datenseite. Hierbei wird das Format File_Id:Page_Id:Slot_Id verwendet.
RECORDSET_01
Wie aus der Abbildung ersichtlich ist, werden die Datensätze von Id = 1 bis Id = 13 auf insgesamt 13 Slot(s) auf der Datenseite 904 in der Datenbank gespeichert. Der nachfolgende Datensatz ([Id] = 14) wird bereits auf Datenseite 906 in Slot 0 gespeichert. Für das weitere Vorgehen ist es erforderlich, den Inhalt einer Datenseite etwas genauer unter die Lupe zu nehmen. Insbesondere der Page Header sowie ein Beispieldatensatz (Datenseite 904, Slot 9) sollen genauer beleuchtet werden.

 

   1: -- Ausgabe des Page Headers sowie eines Hexdumps der Daten und Slots

   2: DBCC TRACEON (3604);

   3: DBCC PAGE (19, 1, 904, 1);


Page Header


DBCC_PAGE_01


m_pageIdNummer der Datenseite
m_SlotCntDie Anzahl der belegten Slot(s) auf der Datenseite
m_freeCntAnzahl der verfügbaren Bytes auf der Datenseite
m_freeDataOffset des nächsten freien Bereichs auf der Datenseite ab dem ein neuer Datensatz gespeichert werden kann.

Auf der Datenseite 904 sind 13 Slot(s) durch Datensätze belegt. Insgesamt stehen noch 1.284 Bytes zur Speicherung von Daten zur Verfügung. Neue Datensätze können ab Byte 6.882 gespeichert werden.

Record Data


DBCC_PAGE_02

Die Abbildung zeigt den vollständigen Datensatz ([Id] = 10) aus Slot 9. Der Datensatz allokiert 525 Bytes. Interessant ist der Eintrag für [col1]. Man kann erkennen, dass nach der Zeichenkette “AAAAAAAAAA” weitere Leerzeichen (0x20) gespeichert werden. Grund für dieses Verhalten ist die Definition einer festen Länge von 500 Zeichen für das Attribut. Vollständig anders sieht es bei der Speicherung der Daten für [col2] aus -  hierbei handelt es sich um ein Attribut mit variabler Zeichenlänge und es wird ausschließlich der Speicher allokiert, den die eingetragene Zeichenkette belegt.

Offset


 DBCC_PAGE_03

Ein Offset beschreibt die Slot(s), in denen die Datensätze gespeichert werden. Hierbei ist zu erkenne, dass mit dem Datensatz aus Slot 0 bei Byte 96 begonnen wird. Der Page Header hat eine Größe von 96 Bytes (0x00 – 0xF5) und unmittelbar nach dem Page Header beginnt Microsoft SQL Server mit der Speicherung der Daten. Der Offset für den Datensatz aus Slot 9 ([Id] = 10) beginnt bei Byte 4776!

Wie genau Microsoft SQL Server bei der Aktualisierung von Daten in einem HEAP vorgeht und welche Operationen bei einer solchen Aktion durchgeführt werden, habe ich bereits sehr ausführlich im Artikel “UPDATE in HEAP = DELETE mit nachfolgendem INSERT?” beschrieben. In diesem Artikel geht es ausschließlich um die Untersuchung, wie ein FORWARDED RECORD seine neue Allokation auf den Datenseiten abspeichert.

Tests


Im ersten Test wird das Attribut [col2] des Datensatzes [Id] = 10 auf eine Zeichenkette mit der Länge von 1.200 Bytes aktualisiert. Wie aus dem Page Header erkennbar, stehen insgesamt noch 1.284 Bytes auf der Datenseite zur Verfügung. Der Datensatz sollte also problemlos auf der Datenseite gespeichert werden.


 

   1: -- Aktualisierung von Id = 10

   2: UPDATE  dbo.tbl_Heap

   3: SET     col2 = REPLICATE ('Z', 1200)

   4: WHERE   Id = 10;


DBCC_PAGE_04

Man kann an den Offsets erkennen, was passiert ist. Der Datensatz aus Slot 9 wurde um insgesamt 1190 Zeichen im Attribut [col2]erweitert. Diese Erweiterung passte nicht in den bereits allokierten Bereich. Microsoft SQL Server hat den Datensatz aus dem allokierten Bereich entfernt und bei Offset 6.357 erneut abgespeichert. Interessant ist bei Evaluierung der Offsets, dass die Daten aus Slot 10 – Slot 12 ebenfalls verschoben wurden!

Im nächsten Test wird der Datensatz mit [Id] = 10 erneut aktualisiert. Nun wird der Eintrag im Attribut [col2] auf 1.500 Zeichen erweitert. Auf Datenseite 904 stehen noch weitere 94 Bytes zur Verfügung. Die neuen Daten können also nicht auf Datenseite 904 abgespeichert werden – ein FORWARDED_RECORD wird zwangsläufig erzeugt!


 

   1: -- Aktualisierung von Id = 10

   2: UPDATE dbo.tbl_Heap

   3: SET    col2 = REPLICATE ('Z', 1500)

   4: WHERE  Id = 10;


DBCC_PAGE_05

Der Auszug aus der Datenseite zeigt den Inhalt von Slot 9 der betroffenen Datenseite. Der Offset hat sich nicht verändert, jedoch sind die ursprünglichen Daten einem “kryptischen” Hexadezimalwert gewichen. Dieser Hexadezimalwert beschreibt vier wichtige Eigenschaften des ursprünglichen Datensatzes:

OffsetBeschreibung
Byte 0:
0x04
Definition des Datensatztyps
0x04 = FORWARDED_RECORD
Bytes 1 – 4:
0x8d 03 00 00
Pointer zur Datenseite, auf der sich der FORWARDED_RECORD befindet.Dezimalwert = 909 = Datenseite 909
Byte 5 - 6:
0x0100
Pointer zur Dateinummer, in der sich die Datenseite befindet
0x0100 = Dateinummer: 1
Bytes 6 – 8:
0x0000
Pointer zum Slot, in dem das Offset gespeichert ist, an dem der Datensatz gespeichert wurde
0x0000 = Slot 0

Basierend auf den Auswertungen kann bestimmt werden, auf welcher Datenseite sich der Datensatz nun befindet!

DBCC_PAGE_06

Microsoft SQL Server hat für den FORWARDED_RECORD eine neue Datenseite erzeugt und den Datensatz auf dieser Datenseite gespeichert. In der gleichen Transaktion mussten auf Datenseite 904 Modifikationen für Slot 9 durchgeführt werden.

Was passiert jedoch, wenn der Datensatz erneut nicht auf die Datenseite passt und wieder eine neue Datenseite allokieren muss? Genau diese Frage wurde immer wieder damit beantwortet, dass dann – bleiben wir beim obigen Beispiel – auf Datenseite 909 erneut ein Verweis auf den neuen Speicherort hinterlegt wird. Somit müsste Microsoft SQL Server dann von Datenseite 904 (Ursprung) zunächst auf Datenseite 909 “springen” um dort erneut eine Adresse zu finden, an der sich der Datensatz befindet – quasi eine “Schnitzeljagd”.

Um diesen Fall zu widerlegen, wird zunächst ein weiterer Datensatz in die Relation eingetragen. In einem HEAP werden neue Datensätze immer auf Datenseiten eingefügt, die ausreichend Platz für die Speicherung zur Verfügung stellen (siehe auch “Neue Daten in einem HEAP eintragen – immer auf der letzten Seite?”. Da im aktuellen Beispiel auf der Datenseite 909 ausreichend Platz vorhanden ist, wird der Datensatz in dieser Datenseite gespeichert!


 

   1: -- Ein neuer Datensatz wird eingetragen

   2: INSERT INTO dbo.tbl_Heap (col1, Col2)

   3: VALUES ('Uwe Ricken', REPLICATE ('Y', 2000));


Ein erneuter Blick auf den Page Header von Seite 909 zeigt, dass die Annahme korrekt war. Slot 2 wird von dem zuvor eingegebenen Datensatz allokiert.

DBCC_PAGE_07

Insgesamt sind nun 2 Slot(s) allokiert und es stehen noch weitere 3.550 auf der Datenseite zur Verfügung. Der bereits weitergeleitete Datensatz ([Id] = 10) wird nun erneut aktualisiert. Diesmal wird die Zeichenkette in [col2] auf 7.500 Zeichen verlängert und anschließend der Page Header sowie die Offsets der Datenseite 909 untersucht. Stimmt die Aussage, dass auf jeder Datenseite, auf der ein FORWARDED_RECORD vormals gespeichert wurde, ein weiterer Verweis auf den Verbleib des Datensatzes gespeichert wird, müssten diese Informationen auf Datenseite 909 zu sehen sein.


 

   1: -- Aktualisierung von Id = 10

   2: UPDATE dbo.tbl_Heap

   3: SET    col2 = REPLICATE ('Z', 7500)

   4: WHERE  Id = 10;

   5:  

   6: -- Ausgabe des Page Headers sowie eines Hexdumps der Daten und Slots

   7: DBCC TRACEON (3604);

   8: DBCC PAGE (19, 1, 909, 1);


Der Blick auf den Page Header von Datenseite 909 lässt vermuten, dass die Verfechter dieser These Recht haben; es gibt zwei allokierte Slot(s).

DBCC_PAGE_08

Auch die Informationen bezüglich des zur Verfügung stehenden Speichers geben keine ausreichenden Informationen, die gegen diese These sprechen. Ein Blick auf die Offsets zeigt aber, was tatsächlich passiert ist.

DBCC_PAGE_09

Zunächst wurde der – ursprünglich auf Datenseite 904 gespeicherte – Datensatz auf Datenseite 909 verschoben. Dort hat der betroffene Datensatz Slot 0 allokiert. Anschließend wurde ein weiterer Datensatz hinzugefügt – der wiederum Slot 1 belegt. Zu guter Letzt wurde der ursprünglich in Slot 0 allokierte Datensatz erneut aktualisiert und – erneut verschoben; Offset 0 ist LEER. Es sind für Offset 0 keine verwertbaren Informationen vorhanden. Auch ein Blick auf den Hexdump zeigt keine Informationen in Bezug auf den verschobenen Datensatz. Diese Information lässt den Schluss zu, dass erneut im Ursprung (Datenseite 904) mit der Suche begonnen werden muss. Der Hexdump für Slot 9 auf Datenseite 904 sieht wie folgt aus:


 

   1: -- Ausgabe des Page Headers sowie eines Hexdumps der Daten und Slots

   2: DBCC TRACEON (3604);

   3: DBCC PAGE (19, 1, 904, 1);


DBCC_PAGE_10

Der Pointer zum FORWARDED_RECORD muss erneut gemäß den Strukturrichtlinien entschlüsselt werden:

OffsetBeschreibung
Byte 0: 0x04Definition des Datensatztyps0x04 = FORWARDED_RECORD
Bytes 1 – 4:
0x8E 03 00 00
Pointer zur Datenseite, auf der sich der FORWARDED_RECORD befindet.Dezimalwert = 910 = Datenseite 910
Byte 5 - 6:
0x0100
Pointer zur Dateinummer, in der sich die Datenseite befindet0x01 = Dateinummer: 1
Bytes 7 – 8:
0x0000
Pointer zum Slot, in dem das Offset gespeichert ist, an dem der Datensatz gespeichert wurde0x00 00 00 = Slot 0

Gemäß der entschlüsselten Informationen ist der Datensatz nicht mehr auf Datenseite 909 sondern auf Datenseite 910.

DBCC_PAGE_11

Zum Schluss wird der Datensatz mit [Id] = 9 erneut geändert und erhält sein ursprüngliches Datenvolumen zurück.


 

   1: -- Aktualisierung von Id = 10

   2: UPDATE dbo.tbl_Heap

   3: SET    col2 = REPLICATE ('Z', 10)

   4: WHERE  Id = 10;

   5:  

   6: -- Ausgabe des Page Headers sowie eines Hexdumps der Daten und Slots

   7: DBCC TRACEON (3604);

   8: DBCC PAGE (19, 1, 904, 1);


DBCC_PAGE_12

Fazit


Das obige Beispiel hat deutlich gezeigt, dass keine “Schnitzeljagd” stattfindet. Wenn ein Datensatz in einem HEAP zu einem FORWARDED_RECORD wird, wird auf der ursprünglichen Datenseite ein Verweis zum aktuellen Speicherplatz gespeichert. Wird ein FORWARDED_RECORD immer wieder auf weitere Datenseiten verschoben, werden ausschließlich am ursprünglichen Speicherort des Datensatzes diese Informationen hinterlegt. Sobald ein Datensatz wieder in den vormals allokierten Speicherbereich passen sollte, wird der Status eines FORWARDED_RECORD wieder aufgehoben. Diese Option ist jedoch nur möglich, wenn der HEAP nicht zwischendurch neu aufgebaut wurde (REBUILD)!

Herzlichen Dank fürs Lesen

HEAP-Strukturenhttp://technet.microsoft.com/en-us/library/ms188270.aspx
Clustered Index:http://technet.microsoft.com/en-us/library/ms177443.aspx

Kommentare :

  1. Guter Artikel - vielen Dank!

    Meiner Erfahrung nach ist die Auflösung des FORWARDING_RECORDS wie folgt:

    Page 4 Bytes
    FileId 2 Bytes
    Slot 2 Bytes

    Sollte ja analog des RIDs sein.

    "stehen einer Datenseite (8.192 Bytes) für Daten 8.060 Bytes und für Slots 36 Bytes zur Verfügung. "
    Zudem ist das ROW OFFSET mindestens 36 Bytes groß. Der Eintrag für einen Datensatz allokiert 2 Bytes ín der ROW OFFSET TABLE. Wäre das Offset nur 36 Bytes groß, könnten nur 18 Records gespeichert werden. Das ROW OFFSET wächst mit der Anzahl der DS, sodass weniger Platz in der Payload zur Verfügung steht.

    AntwortenLöschen
  2. Guten Morgen Torsten,

    herzlichen Dank für Deinen Kommentar. Du hast natürlich Recht und der Fehler in der Definition der Datenstruktur wurde von mir geändert. es sind die Bytes 5 + 6, welche die Page-Id bestimmen.

    AntwortenLöschen