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 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.
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).
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.
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.
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
m_pageId | Nummer der Datenseite |
m_SlotCnt | Die Anzahl der belegten Slot(s) auf der Datenseite |
m_freeCnt | Anzahl der verfügbaren Bytes auf der Datenseite |
m_freeData | Offset 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
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
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;
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;
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:
Offset | Beschreibung |
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: 0x01 | Pointer zur Dateinummer, in der sich die Datenseite befindet 0x01 = Dateinummer: 1 |
Bytes 6 – 8: 0x00 00 00 | Pointer zum Slot, in dem das Offset gespeichert ist, an dem der Datensatz gespeichert wurde 0x00 00 00 = Slot 0 |
Basierend auf den Auswertungen kann bestimmt werden, auf welcher Datenseite sich der Datensatz nun befindet!
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 am Ende eingefügt, da es keine Sortierkriterien (Clustered Key) gibt. Der neu einzutragende Datensatz sollte also auf Datenseite 909 in Slot 2 eingetragen werden.
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.
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).
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.
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);
Der Pointer zum FORWARDED_RECORD muss erneut gemäß den Strukturrichtlinien entschlüsselt werden:
Offset | Beschreibung |
Byte 0: 0x04 | Definition des Datensatztyps 0x04 = 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: 0x01 | Pointer zur Dateinummer, in der sich die Datenseite befindet 0x01 = Dateinummer: 1 |
Bytes 6 – 8: 0x00 00 00 | Pointer zum Slot, in dem das Offset gespeichert ist, an dem der Datensatz gespeichert wurde 0x00 00 00 = Slot 0 |
Gemäß der entschlüsselten Informationen ist der Datensatz nicht mehr auf Datenseite 909 sondern auf Datenseite 910.
Zum Schluss wird der Datensatz mit der [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);
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-Strukturen | http://technet.microsoft.com/en-us/library/ms188270.aspx |
Clustered Index: | http://technet.microsoft.com/en-us/library/ms177443.aspx |
Print article | This entry was posted by Uwe Ricken on 21.08.13 at 18:42:00 . Follow any responses to this post through RSS 2.0. |