512 Bytes werden von der 8 KByte-Datenseite von SQL Server nicht verwendet

13

Ich habe die folgende Tabelle erstellt:

CREATE TABLE dbo.TestStructure
(
    id INT NOT NULL,
    filler1 CHAR(36) NOT NULL,
    filler2 CHAR(216) NOT NULL
);

und erstellte dann einen gruppierten Index:

CREATE CLUSTERED INDEX idx_cl_id 
ON dbo.TestStructure(id);

Als nächstes habe ich es mit 30 Zeilen bestückt, jede Größe ist 256 Byte (basierend auf der Tabellendeklaration):

DECLARE @i AS int = 0;

WHILE @i < 30
BEGIN
    SET @i = @i + 1;

    INSERT INTO dbo.TestStructure (id, filler1, filler2)
    VALUES (@i, 'a', 'b');
END;

Basierend auf Informationen, die ich im Buch "Training Kit (Prüfung 70-461): Abfragen von Microsoft SQL Server 2012 (Itzik Ben-Gan)" gelesen habe:

SQL Server organisiert intern Daten in einer Datendatei in Seiten. Eine Seite ist eine 8-KB-Einheit und gehört zu einem einzelnen Objekt. Zum Beispiel zu einer Tabelle oder einem Index. Eine Seite ist die kleinste Lese- und Schreibeinheit. Die Seiten sind weiter in Bereiche unterteilt. Ein Umfang besteht aus acht aufeinander folgenden Seiten. Seiten aus einer Ausdehnung können zu einem einzelnen Objekt oder zu mehreren Objekten gehören. Wenn die Seiten zu mehreren Objekten gehören, wird die Ausdehnung als gemischte Ausdehnung bezeichnet. Wenn die Seiten zu einem einzelnen Objekt gehören, wird die Ausdehnung als einheitliche Ausdehnung bezeichnet. SQL Server speichert die ersten acht Seiten eines Objekts in gemischten Ausmaßen. Wenn ein Objekt mehr als acht Seiten umfasst, weist SQL Server diesem Objekt zusätzliche einheitliche Bereiche zu. Mit dieser Organisation verschwenden kleine Objekte weniger Platz und große Objekte sind weniger fragmentiert.

Hier habe ich also die erste 8-KB-Seite mit gemischtem Umfang, die mit 7680 Bytes gefüllt ist (ich habe eine Zeile mit 30 mal 256 Bytes eingefügt, also 30 * 256 = 7680), um die Größe zu überprüfen, die ich beim Größenprüfprozess ausgeführt habe. Es wird das folgende Ergebnis zurückgegeben

index_type_desc: CLUSTERED INDEX
index_depth: 1
index_level: 0 
page_count: 1 
record_count: 30 
avg_page_space_used_in_percent: 98.1961947121324
name : TestStructure        
rows : 30   
reserved :  16 KB
data : 8 KB 
index_size : 8 KB       
unused :    0 KB

So sind 16 KB für die Tabelle reserviert, die erste 8-KB-Seite ist für die Root-IAM-Seite, die zweite für die Blattdatenspeicherseite, die 8 KB mit einer Belegung von ~ 7,5 KB hat. Jetzt füge ich eine neue Zeile mit 256 Byte ein:

INSERT INTO dbo.TestStructure (id, filler1, filler2)
VALUES (1, 'a', 'b');

es wird nicht auf derselben Seite gespeichert, obwohl es einen Speicherplatz von 256 Byte hat (7680 b + 256 = 7936, was immer noch kleiner als 8 KB ist), eine neue Datenseite wird erstellt, aber diese neue Zeile könnte auf dieselbe alte Seite passen Warum erstellt SQL Server eine neue Seite, wenn dies Platz und Zeit spart, wenn Sie sie in die vorhandene Seite einfügen?

Hinweis: Dasselbe passiert im Heap-Index.

Alphas Supremum
quelle

Antworten:

9

Ihre Datenzeilen sind nicht 256 Bytes. Jedes entspricht eher 263 Bytes. Eine Datenzeile mit rein Datentypen mit fester Länge hat aufgrund der Struktur einer Datenzeile in SQL Server zusätzlichen Overhead. Schauen Sie sich diese Site an und lesen Sie, wie eine Datenzeile aufgebaut ist. http://aboutsqlserver.com/2013/10/15/sql-server-storage-engine-data-pages-and-data-rows/

In Ihrem Beispiel haben Sie also eine Datenzeile mit 256 Bytes, 2 Bytes für Statusbits, 2 Bytes für die Anzahl der Spalten, 2 Bytes für die Datenlänge und eine weitere 1 für die Null-Bitmap. Das sind 263 * 30 = 7.890 Byte. Wenn Sie weitere 263 hinzufügen, überschreiten Sie das Seitenlimit von 8 KB, wodurch eine weitere Seite erstellt werden müsste.

dfundako
quelle
Der von Ihnen bereitgestellte Link hat mir geholfen, die Seitenstruktur besser zu visualisieren. Ich habe nach etwas Ähnlichem gesucht, konnte es aber nicht finden. Thax
Alphas Supremum
11

Zwar verwendet SQL Server 8k (8192 Bytes) Datenseiten zum Speichern von 1 oder mehr Zeilen, doch hat jede Datenseite einen gewissen Overhead (96 Bytes) und jede Zeile einen gewissen Overhead (mindestens 9 Bytes). Die 8192 Bytes sind keine reinen Daten.

Für eine genauere Untersuchung, wie dies funktioniert, lesen Sie bitte meine Antwort auf die folgende DBA.SE-Frage:

SUMME der DATALENGTHs, die nicht mit der Tabellengröße von sys.allocation_units übereinstimmen

Anhand der Informationen in dieser verknüpften Antwort erhalten wir ein klareres Bild der tatsächlichen Zeilengröße:

  1. Zeilenkopf = 4 Bytes
  2. Anzahl der Spalten = 2 Bytes
  3. NULL Bitmap = 1 Byte
  4. Versionsinfo ** = 14 Bytes (optional, siehe Fußnote)
  5. Gesamter Overhead pro Zeile (ohne Slot-Array) = mindestens 7 Byte oder 21 Byte, wenn Versionsinformationen vorhanden sind
  6. Tatsächliche Gesamtzeilengröße = mindestens 263 (256 Daten + 7 Overhead) oder 277 Bytes (256 Daten + 21 Overhead), wenn Versionsinformationen vorhanden sind
  7. Wenn Sie das Slot-Array hinzufügen, beträgt der Gesamtspeicherplatz pro Zeile entweder 265 Bytes (ohne Versionsinfo) oder 279 Bytes (mit Versionsinfo).

Mit wird DBCC PAGEmeine Berechnung mit Record Size 263(für tempdb) und Record Size 277(für eine Datenbank, die auf festgelegt ist ALLOW_SNAPSHOT_ISOLATION ON) bestätigt.

Nun, mit 30 Zeilen, das heißt:

  • OHNE Versionsinfo

    30 * 263 würde uns 7890 Bytes geben. Fügen Sie dann den 96-Byte-Seitenkopf für 7986-Byte hinzu. Fügen Sie abschließend die 60 Bytes (2 pro Zeile) des Slot-Arrays hinzu, sodass insgesamt 8046 Bytes auf der Seite verwendet werden und 146 übrig bleiben. Mit DBCC PAGEbestätigen Sie meine Berechnung mit:

    • m_slotCnt 30 (dh Anzahl der Zeilen)
    • m_freeCnt 146 (dh Anzahl der auf der Seite verbleibenden Bytes)
    • m_freeData 7986 (dh Daten + Seitenkopf - 7890 + 96 - Slot-Array wird bei der Berechnung der "verwendeten" Bytes nicht berücksichtigt)
  • MIT Versionsinfo

    30 * 277 Bytes für insgesamt 8310 Bytes. Aber 8310 ist über 8192, und das hat nicht einmal den 96-Byte-Seitenkopf oder das 2-Byte-Array pro Zeilen-Slot (30 * 2 = 60 Bytes) berücksichtigt, was uns nur 8036 verwendbare Bytes für die Zeilen geben sollte.

    ABER, was ist mit 29 Zeilen? Dies ergibt 8033 Datenbytes (29 * 277) + 96 Bytes für den Seitenkopf + 58 Bytes für das Slot-Array (29 * 2), was 8187 Bytes entspricht. Und das würde die Seite mit 5 verbleibenden Bytes verlassen (8192 - 8187; natürlich unbrauchbar). Mit DBCC PAGEbestätigen Sie meine Berechnung mit:

    • m_slotCnt 29 (dh Anzahl der Zeilen)
    • m_freeCnt 5 (dh Anzahl der auf der Seite verbleibenden Bytes)
    • m_freeData 8129 (dh Daten + Seitenkopf - 8033 + 96 - Slot-Array wird bei der Berechnung der "verwendeten" Bytes nicht berücksichtigt)

In Bezug auf Haufen

Haufen füllen Datenseiten etwas anders. Sie erhalten eine sehr grobe Schätzung des auf der Seite verbleibenden Speicherplatzes. Wenn an der DBCC - Ausgabe suchen, schauen Sie in der Zeile für: PAGE HEADER: Allocation Status PFS (1:1). Sie werden sehen, VALUEdass etwas in der Art von 0x60 MIXED_EXT ALLOCATED 0_PCT_FULL(wenn ich die Clustered-Tabelle betrachte) oder 0x64 MIXED_EXT ALLOCATED 100_PCT_FULLwenn ich die Heap-Tabelle betrachte. Dies wird pro Transaktion ausgewertet, sodass einzelne Einfügungen, wie der hier ausgeführte Test, zu unterschiedlichen Ergebnissen zwischen Clustered- und Heap-Tabellen führen können. Wenn Sie jedoch eine einzelne DML-Operation für alle 30 Zeilen ausführen, wird der Heap wie erwartet gefüllt.

Keine dieser Details in Bezug auf Heaps wirkt sich jedoch direkt auf diesen speziellen Test aus, da beide Versionen der Tabelle 30 Zeilen mit nur 146 verbleibenden Bytes aufnehmen. Das ist nicht genug Platz für eine andere Zeile, unabhängig von Clustered oder Heap.

Bitte beachten Sie, dass dieser Test ziemlich einfach ist. Die Berechnung der tatsächlichen Größe einer Zeile kann in Abhängigkeit von verschiedenen Faktoren sehr kompliziert werden, z. SPARSEB . : , Datenkomprimierung, LOB-Daten usw.


Verwenden Sie die folgende Abfrage, um die Details der Datenseite anzuzeigen:

DECLARE @PageID INT,
        @FileID INT,
        @DatabaseID SMALLINT = DB_ID();

SELECT  @FileID = alloc.[allocated_page_file_id],
        @PageID = alloc.[allocated_page_page_id]
FROM    sys.dm_db_database_page_allocations(@DatabaseID,
                            OBJECT_ID(N'dbo.TestStructure'), 1, NULL, 'DETAILED') alloc
WHERE   alloc.[previous_page_page_id] IS NULL -- first data page
AND     alloc.[page_type] = 1; -- DATA_PAGE

DBCC PAGE(@DatabaseID, @FileID, @PageID, 3) WITH TABLERESULTS;

** Der 14-Byte-Wert "Versionsinfo" ist vorhanden, wenn Ihre Datenbank auf " ALLOW_SNAPSHOT_ISOLATION ONoder" festgelegt ist READ_COMMITTED_SNAPSHOT ON.

Solomon Rutzky
quelle
Für Benutzerdaten stehen 8060 Bytes pro Seite zur Verfügung, um genau zu sein. Die Daten des OP liegen noch darunter.
Roger Wolf
Die Versionsinformationen sind nicht vorhanden, da andernfalls 30 Zeilen 8310 Byte benötigen. Der Rest scheint richtig zu sein.
Roger Wolf
@ RogerWolf Ja, "Versionsinfo" ist da. Also ja, 30 Zeilen benötigen 8310 Bytes. Das ist der Grund, warum diese 30 Zeilen nicht alle auf eine Seite passten, da das OP durch den Testprozess, den das OP verwendet, zu dem Glauben verleitet wird. Aber dieser Testprozess ist falsch. Nur 29 Zeilen passen auf die Seite. Ich habe dies bestätigt (sogar mit SQL Server 2012).
Solomon Rutzky
Haben Sie versucht, Ihren Test auf einer Datenbank auszuführen, die nicht RCSI-fähig ist tempdb? Ich konnte die genauen Zahlen von OP reproduzieren.
Roger Wolf
@ RogerWolf Die von mir verwendete Datenbank ist nicht RCSI-fähig, aber auf gesetzt ALLOW_SNAPSHOT_ISOLATION. Ich habe auch gerade versucht tempdbund gesehen, dass die "Versionsinfo" nicht da ist, daher passen 30 Zeilen. Ich werde aktualisieren, um die neuen Informationen hinzuzufügen.
Solomon Rutzky
3

Die eigentliche Struktur der Datenseite ist recht komplex. Während im Allgemeinen angegeben wird, dass 8060 Byte pro Seite für Benutzerdaten verfügbar sind, gibt es einen zusätzlichen Overhead, der nirgendwo gezählt wird, was zu diesem Verhalten führt.

Möglicherweise haben Sie jedoch bemerkt, dass SQL Server Ihnen einen Hinweis gibt, dass die 31. Zeile nicht in die Seite passt. Damit die nächste Zeile auf dieselbe Seite passt, sollte der avg_page_space_used_in_percentWert unter 100% liegen - (100/31) = 96,774194, und in Ihrem Fall liegt er weit darüber.

PS: Ich glaube, ich habe in einem der "SQL Server Internals" -Bücher von Kalen Delaney eine ausführliche, bis ins kleinste Byte genaue Erklärung der Datenseitenstruktur gesehen, aber es war fast 10 Jahre her. Außerdem ändert sich die Seitenstruktur in der Regel von Version zu Version.

Roger Wolf
quelle
1
Nein. Der Uniquifier wird nur hinzugefügt, um Schlüsselzeilen zu duplizieren. In der ersten Zeile jedes eindeutigen Schlüsselwerts ist der zusätzliche 4-Byte-Eindeutiger nicht enthalten.
Solomon Rutzky
@srutzky, anscheinend hast du recht. Ich hätte nie gedacht, dass SQL Server Schlüssel mit variabler Breite zulässt. Das ist hässlich. Effizient, ja, aber hässlich.
Roger Wolf