Please note that the following info is not intended to be a comprehensive
description of how data pages are laid out, such that one can calculate
the number of bytes used per any set of rows, as that is very complicated.
Daten sind nicht das einzige, was Speicherplatz auf einer 8k-Datenseite beansprucht:
Es ist Platz reserviert. Sie dürfen nur 8060 der 8192 Bytes verwenden (das sind 132 Bytes, die Ihnen überhaupt nicht gehörten):
- Seitenkopf: Dies sind genau 96 Bytes.
- Slot-Array: Dies sind 2 Bytes pro Zeile und gibt den Versatz an, an dem jede Zeile auf der Seite beginnt. Die Größe dieses Arrays ist nicht auf die verbleibenden 36 Bytes beschränkt (132 - 96 = 36), andernfalls können Sie effektiv nur maximal 18 Zeilen auf eine Datenseite setzen. Dies bedeutet, dass jede Zeile 2 Byte größer ist als Sie denken. Dieser Wert ist nicht in der "Datensatzgröße" enthalten, wie von angegeben
DBCC PAGE
, weshalb er hier getrennt gehalten wird, anstatt in den nachstehenden Informationen pro Zeile enthalten zu sein.
- Pro-Zeile-Metadaten (einschließlich, aber nicht beschränkt auf):
- Die Größe variiert je nach Tabellendefinition (dh Anzahl der Spalten, variable Länge oder feste Länge usw.). Informationen aus den Kommentaren von @ PaulWhite und @ Aaron, die in der Diskussion zu dieser Antwort zu finden sind und den Tests zu finden sind.
- Zeilenüberschrift: 4 Bytes, von denen 2 den Datensatztyp angeben und die anderen beiden einen Offset zur NULL-Bitmap darstellen
- Anzahl der Spalten: 2 Bytes
- NULL Bitmap: welche Spalten sind zur Zeit
NULL
. 1 Byte pro Satz von 8 Spalten. Und für alle Spalten, auch für die NOT NULL
. Daher mindestens 1 Byte.
- Spaltenversatzarray mit variabler Länge: mindestens 4 Byte. 2 Bytes für die Anzahl der Spalten mit variabler Länge und dann 2 Bytes pro Spalte mit variabler Länge für den Versatz an der Stelle, an der er beginnt.
- Versionsinformationen: 14 Bytes (diese sind vorhanden, wenn Ihre Datenbank auf entweder
ALLOW_SNAPSHOT_ISOLATION ON
oder eingestellt ist READ_COMMITTED_SNAPSHOT ON
).
- Weitere Informationen hierzu finden Sie in der folgenden Frage und Antwort: Slot-Array und Gesamtseitengröße
- Bitte lesen Sie den folgenden Blog-Beitrag von Paul Randall, der einige interessante Details zum Aufbau der Datenseiten enthält : Stöbern mit DBCC-SEITE (Teil 1 von?)
LOB-Zeiger für Daten, die nicht in einer Zeile gespeichert sind. Das würde also DATALENGTH
+ pointer_size ausmachen. Diese haben jedoch keine Standardgröße. Weitere Informationen zu diesem komplexen Thema finden Sie im folgenden Blog-Beitrag: Wie groß ist der LOB-Zeiger für (MAX) Typen wie Varchar, Varbinary usw.? . Zwischen diesem verknüpften Beitrag und einigen zusätzlichen Tests, die ich durchgeführt habe , sollten die (Standard-) Regeln wie folgt lauten:
- Legacy / veraltete LOB - Typen , dass niemand mehr als von SQL Server 2005 verwenden soll (
TEXT
, NTEXT
und IMAGE
):
- Speichern Sie ihre Daten standardmäßig immer auf LOB-Seiten und verwenden Sie immer einen 16-Byte-Zeiger auf den LOB-Speicher.
- WENN sp_tableoption verwendet wurde, um die
text in row
Option festzulegen , dann:
- wenn auf der Seite Platz zum Speichern des Werts vorhanden ist, und Wert nicht größer als die maximale Zeilengröße ist (konfigurierbarer Bereich von 24 bis 7000 Byte mit einem Standardwert von 256), wird er in der Zeile gespeichert.
- Andernfalls handelt es sich um einen 16-Byte-Zeiger.
- Für die neueren LOB - Typen in SQL Server 2005 eingeführt (
VARCHAR(MAX)
, NVARCHAR(MAX)
und VARBINARY(MAX)
):
- Standardmäßig:
- Wenn der Wert nicht größer als 8000 Byte ist und auf der Seite Platz vorhanden ist, wird er in einer Reihe gespeichert.
- Inline-Root - Für Daten zwischen 8001 und 40.000 (wirklich 42.000) Bytes, sofern der Speicherplatz dies zulässt, befinden sich 1 bis 5 Zeiger (24 - 72 Bytes) IN ROW, die direkt auf die LOB-Seite (n) verweisen. 24 Bytes für die erste 8k-LOB-Seite und 12 Bytes für jede weitere 8k-Seite für bis zu vier weitere 8k-Seiten.
- TEXT_TREE - Für Daten über 42.000 Byte oder wenn die 1 bis 5 Zeiger nicht in eine Zeile passen, gibt es nur einen 24-Byte-Zeiger auf die Startseite einer Liste von Zeigern auf die LOB-Seiten (dh den "text_tree" " Seite).
- IF sp_tableoption verwendet wurde , die festlegen
large value types out of row
Option, dann verwenden Sie immer ein 16-Byte - Zeiger auf LOB Speicher.
- Ich sagte : „default“ Regeln , weil ich habe nicht in-Zeilenwerte gegen die Auswirkungen bestimmter Funktionen testen, wie Datenkomprimierung, Verschlüsselung auf Spaltenebene, Transparent Data Encryption, immer verschlüsselt, usw.
LOB-Überlaufseiten: Wenn ein Wert 10.000 beträgt, ist dafür eine vollständige 8.000-Seite Überlauf und dann ein Teil einer zweiten Seite erforderlich. Wenn keine anderen Daten den verbleibenden Speicherplatz belegen können (oder sogar dürfen, da bin ich mir dieser Regel nicht sicher), haben Sie ca. 6 KB "verschwendeten" Speicherplatz auf dieser 2. LOB-Überlauf-Datenseite.
Nicht genutzter Speicherplatz: Eine 8k-Datenseite ist genau das: 8192 Bytes. Es variiert nicht in der Größe. Die darauf platzierten Daten und Metadaten passen jedoch nicht immer gut in alle 8192 Bytes. Und Zeilen können nicht auf mehrere Datenseiten aufgeteilt werden. Wenn Sie also noch 100 Bytes haben, aber keine Zeile (oder keine Zeile, die in Abhängigkeit von mehreren Faktoren an diesen Speicherort passen würde) dort passen kann, belegt die Datenseite immer noch 8192 Bytes, und Ihre zweite Abfrage zählt nur die Anzahl von Datenseiten. Sie können diesen Wert an zwei Stellen finden (denken Sie daran, dass ein Teil dieses Werts ein Teil des reservierten Speicherplatzes ist):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Suchen Sie nach ParentObject
= "PAGE HEADER:" und Field
= "m_freeCnt". Das Value
Feld gibt die Anzahl der nicht verwendeten Bytes an.
SELECT buff.free_space_in_bytes FROM sys.dm_os_buffer_descriptors buff WHERE buff.[database_id] = DB_ID(N'db_name') AND buff.[page_id] = page_id;
Dies ist der gleiche Wert wie von "m_freeCnt" angegeben. Dies ist einfacher als DBCC, da es viele Seiten erhalten kann, erfordert aber auch, dass die Seiten zuerst in den Pufferpool eingelesen wurden.
Von FILLFACTOR
<100 reservierter FILLFACTOR
Speicherplatz . Neu erstellte Seiten berücksichtigen die Einstellung nicht. Bei einem REBUILD wird dieser Speicherplatz jedoch auf jeder Datenseite reserviert. Die Idee hinter dem reservierten Speicherplatz ist, dass er von nicht sequentiellen Einfügungen und / oder Aktualisierungen verwendet wird, die die Größe der Zeilen auf der Seite bereits erweitern, da Spalten variabler Länge mit etwas mehr Daten aktualisiert werden (aber nicht genug, um a zu verursachen) Seitenaufteilung). Sie können jedoch problemlos Speicherplatz auf Datenseiten reservieren, die natürlich niemals neue Zeilen erhalten und die vorhandenen Zeilen niemals aktualisieren oder zumindest nicht auf eine Weise aktualisiert werden, die die Größe der Zeile erhöht.
Seitenteilung (Fragmentierung): Wenn Sie eine Zeile an einer Stelle hinzufügen müssen, an der kein Platz für die Zeile vorhanden ist, wird die Seite geteilt. In diesem Fall werden ca. 50% der vorhandenen Daten auf eine neue Seite verschoben und die neue Zeile zu einer der beiden Seiten hinzugefügt. Aber Sie haben jetzt etwas mehr freien Speicherplatz, der nicht durch DATALENGTH
Berechnungen berücksichtigt wird.
Zum Löschen markierte Zeilen. Wenn Sie Zeilen löschen, werden diese nicht immer sofort von der Datenseite entfernt. Wenn sie nicht sofort entfernt werden können, sind sie "für den Tod markiert" (Steven Segal-Referenz) und werden später durch den Geisterbereinigungsprozess physisch entfernt (ich glaube, das ist der Name). Diese sind jedoch möglicherweise für diese spezielle Frage nicht relevant.
Ghost-Seiten? Ich bin mir nicht sicher, ob dies der richtige Begriff ist, aber manchmal werden Datenseiten erst entfernt, wenn eine Neuerstellung des Clustered-Index durchgeführt wurde. Das würde auch mehr Seiten ausmachen, als DATALENGTH
sich summieren würden. Dies sollte im Allgemeinen nicht passieren, aber ich bin vor einigen Jahren einmal darauf gestoßen.
SPARSE-Spalten: Sparse-Spalten sparen Platz (hauptsächlich für Datentypen mit fester Länge) in Tabellen, in denen ein großer Teil der Zeilen NULL
für eine oder mehrere Spalten bestimmt ist. Die SPARSE
Option macht den NULL
Werttyp mit 0 Bytes (anstelle der normalen fester Länge Menge, wie beispielsweise 4 Bytes für eine INT
), aber , nicht-NULL nehmen jeweils Werte bis weitere 4 Bytes für feste Länge Typen und einen variablen Betrag für Typen mit variabler Länge. Das Problem hierbei ist, dass DATALENGTH
die zusätzlichen 4 Bytes für Nicht-NULL-Werte in einer SPARSE-Spalte nicht enthalten sind. Daher müssen diese 4 Bytes wieder hinzugefügt werden. Sie können überprüfen, ob SPARSE
Spalten vorhanden sind über:
SELECT OBJECT_SCHEMA_NAME(sc.[object_id]) AS [SchemaName],
OBJECT_NAME(sc.[object_id]) AS [TableName],
sc.name AS [ColumnName]
FROM sys.columns sc
WHERE sc.is_sparse = 1;
SPARSE
Aktualisieren Sie dann für jede Spalte die ursprüngliche Abfrage, um Folgendes zu verwenden:
SUM(DATALENGTH(FieldN) + 4)
Bitte beachten Sie, dass die obige Berechnung zum Hinzufügen von 4 Standardbytes etwas vereinfacht ist, da sie nur für Typen mit fester Länge funktioniert. UND, es gibt zusätzliche Metadaten pro Zeile (soweit ich das beurteilen kann), die den verfügbaren Speicherplatz für Daten reduzieren, indem einfach mindestens eine SPARSE-Spalte vorhanden ist. Weitere Informationen finden Sie auf der MSDN-Seite für die Verwendung sparsamer Spalten .
Index- und andere (z. B. IAM-, PFS-, GAM-, SGAM- usw.) Seiten: Dies sind keine "Datenseiten" in Bezug auf Benutzerdaten. Dadurch wird die Gesamtgröße des Tisches aufgeblasen. Wenn Sie SQL Server 2012 oder höher verwenden, können Sie die sys.dm_db_database_page_allocations
Dynamic Management Function (DMF) verwenden, um die Seitentypen anzuzeigen (frühere Versionen von SQL Server können Folgendes verwenden DBCC IND(0, N'dbo.table_name', 0);
):
SELECT *
FROM sys.dm_db_database_page_allocations(
DB_ID(),
OBJECT_ID(N'dbo.table_name'),
1,
NULL,
N'DETAILED'
)
WHERE page_type = 1; -- DATA_PAGE
Weder das DBCC IND
noch sys.dm_db_database_page_allocations
(mit dieser WHERE-Klausel) meldet Indexseiten, und nur das DBCC IND
meldet mindestens eine IAM-Seite.
DATA_COMPRESSION: Wenn Sie ROW
oder die PAGE
Komprimierung für den Clustered Index oder Heap aktiviert haben , können Sie das meiste vergessen, was bisher erwähnt wurde. Der 96-Byte-Seitenkopf, das Slot-Array mit 2 Bytes pro Zeile und die Versionsinformationen für 14 Bytes pro Zeile sind noch vorhanden, aber die physische Darstellung der Daten wird sehr komplex (viel mehr als bei der Komprimierung bereits erwähnt) wird nicht verwendet). Bei der Zeilenkomprimierung versucht SQL Server beispielsweise, den kleinstmöglichen Container für jede Spalte pro Zeile zu verwenden. Wenn Sie also eine haben , werden nur 2 Bytes benötigt. Ganzzahlige Typen, die entweder oder keinen Platz beanspruchen und einfach als oder "leer" angezeigt werden (dhBIGINT
Spalte haben, die andernfalls (vorausgesetzt, sie SPARSE
ist auch nicht aktiviert) immer 8 Bytes belegt, wenn der Wert zwischen -128 und 127 liegt (dh eine vorzeichenbehaftete 8-Bit-Ganzzahl), wird nur 1 Byte verwendet, und wenn die Wert könnte in eine passenSMALLINT
NULL
0
NULL
0
) in einem Array, das die Spalten abbildet. Und es gibt viele, viele andere Regeln. Haben Sie Unicode-Daten ( NCHAR
, NVARCHAR(1 - 4000)
aber nicht NVARCHAR(MAX)
, auch wenn sie in einer Reihe gespeichert sind)? Die Unicode-Komprimierung wurde in SQL Server 2008 R2 hinzugefügt, es gibt jedoch keine Möglichkeit, das Ergebnis des "komprimierten" Werts in allen Situationen vorherzusagen, ohne die tatsächliche Komprimierung durchzuführen, da die Regeln komplex sind .
Ihre zweite Abfrage ist zwar genauer in Bezug auf den gesamten physischen Speicherplatz auf der Festplatte, aber nur dann wirklich genau, wenn Sie einen REBUILD
Clustered-Index erstellen. Und danach müssen Sie noch alle berücksichtigenFILLFACTOR
Einstellungen unter 100 . Und selbst dann gibt es immer Seitenkopfzeilen und oft genug eine Menge "verschwendeten" Speicherplatzes, der einfach nicht ausfüllbar ist, weil er zu klein ist, um in eine Zeile zu passen Tabelle oder zumindest die Zeile, die logischerweise in diesen Steckplatz gehen soll.
In Bezug auf die Genauigkeit der zweiten Abfrage bei der Bestimmung der "Datennutzung" erscheint es am fairsten, die Seitenkopf-Bytes zurückzusetzen, da es sich nicht um Datennutzung handelt: Es handelt sich um Betriebskosten. Wenn eine Datenseite 1 Zeile enthält und diese Zeile nur eine ist TINYINT
, ist für dieses 1 Byte immer noch erforderlich, dass die Datenseite vorhanden ist, und daher die 96 Bytes des Headers. Sollte diese 1 Abteilung für die gesamte Datenseite belastet werden? Wenn diese Datenseite dann von Abteilung 2 ausgefüllt wird, würden sie diese "Gemeinkosten" gleichmäßig aufteilen oder proportional zahlen? Scheint am einfachsten, es einfach zurückzuziehen. In diesem Fall ist die Verwendung eines Wertes von 8
zum Multiplizieren number of pages
zu hoch. Wie wäre es mit:
-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250
Verwenden Sie daher etwas wie:
(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
für alle Berechnungen für "number_of_pages" -Spalten.
UND , wenn man bedenkt, dass die Verwendung DATALENGTH
pro Feld die Metadaten pro Zeile nicht zurückgeben kann, sollte dies zu Ihrer Abfrage pro Tabelle hinzugefügt werden, in der Sie die Daten DATALENGTH
pro Feld erhalten und nach jeder "Abteilung" filtern:
- Datensatztyp und Offset zu NULL Bitmap: 4 Bytes
- Spaltenanzahl: 2 Bytes
- Slot Array: 2 Bytes (nicht in "Datensatzgröße" enthalten, müssen aber noch berücksichtigt werden)
- NULL-Bitmap: 1 Byte pro 8 Spalten (für alle Spalten)
- Zeilenversionierung: 14 Bytes (wenn die Datenbank entweder
ALLOW_SNAPSHOT_ISOLATION
oder READ_COMMITTED_SNAPSHOT
gesetzt hat ON
)
- Offset-Array für Spalten variabler Länge: 0 Byte, wenn alle Spalten eine feste Länge haben. Wenn Spalten eine variable Länge haben, dann 2 Bytes plus 2 Bytes pro Spalte mit variabler Länge.
- LOB-Zeiger: Dieser Teil ist sehr ungenau, da es keinen Zeiger gibt, wenn der Wert ist
NULL
, und wenn der Wert in die Zeile passt, kann er viel kleiner oder viel größer als der Zeiger sein, und wenn der Wert nicht gespeichert ist. Zeile, dann kann die Größe des Zeigers davon abhängen, wie viele Daten vorhanden sind. Da wir jedoch nur eine Schätzung wollen (dh "swag"), scheint es, dass 24 Bytes ein guter Wert sind (na ja, so gut wie jeder andere ;-). Dies ist pro MAX
Feld.
Verwenden Sie daher etwas wie:
Im Allgemeinen (Zeilenüberschrift + Anzahl der Spalten + Slot-Array + NULL-Bitmap):
([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
Im Allgemeinen (automatische Erkennung, wenn "Versionsinformationen" vorhanden sind):
+ (SELECT CASE WHEN snapshot_isolation_state = 1 OR is_read_committed_snapshot_on = 1
THEN 14 ELSE 0 END FROM sys.databases WHERE [database_id] = DB_ID())
Wenn Spalten mit variabler Länge vorhanden sind, fügen Sie Folgendes hinzu:
+ 2 + (2 * {NumVariableLengthColumns})
Wenn es irgendwelche MAX
/ LOB-Spalten gibt, fügen Sie hinzu:
+ (24 * {NumLobColumns})
Allgemein:
)) AS [MetaDataBytes]
Dies ist nicht genau und funktioniert auch dann nicht, wenn Sie die Zeilen- oder Seitenkomprimierung für den Heap- oder Clustered-Index aktiviert haben, sollte Sie aber auf jeden Fall näher bringen.
UPDATE In Bezug auf das 15% Unterschiedsgeheimnis
Wir (ich selbst eingeschlossen) waren so darauf konzentriert, darüber nachzudenken, wie Datenseiten angeordnet sind und wie DATALENGTH
Dinge berücksichtigt werden könnten, dass wir nicht viel Zeit damit verbracht haben, die zweite Abfrage zu überprüfen. Ich habe diese Abfrage für eine einzelne Tabelle ausgeführt und diese Werte dann mit den Werten verglichen, die von gemeldet wurden, sys.dm_db_database_page_allocations
und sie waren nicht dieselben Werte für die Anzahl der Seiten. Aus einer Ahnung heraus entfernte ich die Aggregatfunktionen und GROUP BY
und ersetzte die SELECT
Liste durch a.*, '---' AS [---], p.*
. Und dann wurde klar: Die Leute müssen vorsichtig sein, woher sie in diesen düsteren Interwebs ihre Informationen und Skripte beziehen ;-). Die zweite in der Frage gepostete Abfrage ist nicht genau korrekt, insbesondere für diese spezielle Frage.
Kleines Problem: Abgesehen davon, dass es nicht viel Sinn macht GROUP BY rows
(und diese Spalte nicht in einer Aggregatfunktion hat), ist die Verbindung zwischen sys.allocation_units
und sys.partitions
technisch nicht korrekt. Es gibt drei Arten von Zuordnungseinheiten, von denen eine einem anderen Feld beitreten sollte. Sehr oft partition_id
und hobt_id
gleich, daher gibt es möglicherweise nie ein Problem, aber manchmal haben diese beiden Felder unterschiedliche Werte.
Hauptproblem: Die Abfrage verwendet das used_pages
Feld. Dieses Feld deckt alle Arten von Seiten ab: Daten, Index, IAM usw. usw. Es gibt ein anderes, geeigneteres Feld, das nur für die tatsächlichen Daten verwendet werden kann : data_pages
.
Ich habe die zweite Abfrage in der Frage unter Berücksichtigung der oben genannten Punkte und unter Verwendung der Datenseitengröße angepasst, die den Seitenkopf zurücksetzt. Ich habe auch zwei unnötige JOINs entfernt: sys.schemas
(ersetzt durch call to SCHEMA_NAME()
) und sys.indexes
(der Clustered Index ist immer index_id = 1
und wir haben index_id
in sys.partitions
).
SELECT SCHEMA_NAME(st.[schema_id]) AS [SchemaName],
st.[name] AS [TableName],
SUM(sp.[rows]) AS [RowCount],
(SUM(sau.[total_pages]) * 8.0) / 1024 AS [TotalSpaceMB],
(SUM(CASE sau.[type]
WHEN 1 THEN sau.[data_pages]
ELSE (sau.[used_pages] - 1) -- back out the IAM page
END) * 7.91) / 1024 AS [TotalActualDataMB]
FROM sys.tables st
INNER JOIN sys.partitions sp
ON sp.[object_id] = st.[object_id]
INNER JOIN sys.allocation_units sau
ON ( sau.[type] = 1
AND sau.[container_id] = sp.[partition_id]) -- IN_ROW_DATA
OR ( sau.[type] = 2
AND sau.[container_id] = sp.[hobt_id]) -- LOB_DATA
OR ( sau.[type] = 3
AND sau.[container_id] = sp.[partition_id]) -- ROW_OVERFLOW_DATA
WHERE st.is_ms_shipped = 0
--AND sp.[object_id] = OBJECT_ID(N'dbo.table_name')
AND sp.[index_id] < 2 -- 1 = Clustered Index; 0 = Heap
GROUP BY SCHEMA_NAME(st.[schema_id]), st.[name]
ORDER BY [TotalSpaceMB] DESC;