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.
Les données ne sont pas la seule chose qui prend de la place sur une page de données 8k:
Il y a un espace réservé. Vous n'êtes autorisé à utiliser que 8060 des 8192 octets (c'est 132 octets qui ne vous ont jamais appartenu en premier lieu):
- En-tête de page: c'est exactement 96 octets.
- Tableau des emplacements: il s'agit de 2 octets par ligne et indique le décalage de l'endroit où chaque ligne commence sur la page. La taille de ce tableau n'est pas limitée aux 36 octets restants (132 - 96 = 36), sinon vous seriez effectivement limité à ne mettre que 18 lignes maximum sur une page de données. Cela signifie que chaque ligne est 2 octets plus grande que vous ne le pensez. Cette valeur n'est pas incluse dans la "taille d'enregistrement" telle que rapportée par
DBCC PAGE
, c'est pourquoi elle est conservée séparément ici au lieu d'être incluse dans les informations par ligne ci-dessous.
- Métadonnées par ligne (y compris, mais sans s'y limiter):
- La taille varie en fonction de la définition de la table (c'est-à-dire le nombre de colonnes, de longueur variable ou fixe, etc.). Informations tirées des commentaires de @ PaulWhite et @ Aaron qui peuvent être trouvées dans la discussion relative à cette réponse et à ces tests.
- En-tête de ligne: 4 octets, 2 d'entre eux indiquant le type d'enregistrement, et les deux autres étant un décalage par rapport au bitmap NULL
- Nombre de colonnes: 2 octets
- Bitmap NULL: quelles colonnes sont actuellement
NULL
. 1 octet pour chaque ensemble de 8 colonnes. Et pour toutes les colonnes, même NOT NULL
celles. Par conséquent, au moins 1 octet.
- Tableau de décalage de colonne de longueur variable: 4 octets minimum. 2 octets pour contenir le nombre de colonnes de longueur variable, puis 2 octets pour chaque colonne de longueur variable pour conserver le décalage à l'endroit où il commence.
- Informations de version: 14 octets (ce sera présent si votre base de données est définie sur
ALLOW_SNAPSHOT_ISOLATION ON
ou READ_COMMITTED_SNAPSHOT ON
).
- Veuillez consulter les questions et réponses suivantes pour plus de détails à ce sujet: baie de fentes et taille totale de la page
- Veuillez consulter le billet de blog suivant de Paul Randall qui contient plusieurs détails intéressants sur la façon dont les pages de données sont présentées: Fouillez avec DBCC PAGE (Partie 1 de?)
Pointeurs LOB pour les données qui ne sont pas stockées en ligne. Cela représenterait donc DATALENGTH
+ pointer_size. Mais ceux-ci ne sont pas de taille standard. Veuillez consulter le billet de blog suivant pour plus de détails sur ce sujet complexe: Quelle est la taille du pointeur LOB pour les types (MAX) comme Varchar, Varbinary, Etc? . Entre ce message lié et certains tests supplémentaires que j'ai effectués , les règles (par défaut) devraient être les suivantes:
- Legacy / types LOB dépréciée que personne ne devrait être plus que de utilise SQL Server 2005 (
TEXT
, NTEXT
et IMAGE
):
- Par défaut, stockez toujours leurs données sur des pages LOB et utilisez toujours un pointeur de 16 octets vers le stockage LOB.
- SI sp_tableoption a été utilisé pour définir l'
text in row
option, alors:
- s'il y a de l'espace sur la page pour stocker la valeur et que la valeur n'est pas supérieure à la taille maximale en ligne (plage configurable de 24 à 7 000 octets avec une valeur par défaut de 256), elle sera stockée en ligne,
- sinon ce sera un pointeur de 16 octets.
- Pour les nouveaux types de LOB introduites dans SQL Server 2005 (
VARCHAR(MAX)
, NVARCHAR(MAX)
et VARBINARY(MAX)
):
- Par défaut:
- Si la valeur n'est pas supérieure à 8 000 octets et qu'il y a de la place sur la page, elle sera stockée en ligne.
- Racine en ligne - pour les données comprises entre 8001 et 40000 (vraiment 42000) octets, si l'espace le permet, il y aura 1 à 5 pointeurs (24 à 72 octets) EN RANG qui pointent directement vers la ou les pages LOB. 24 octets pour la page LOB initiale de 8 Ko et 12 octets pour chaque page 8 Ko supplémentaire pour un maximum de quatre pages 8 Ko supplémentaires.
- TEXT_TREE - pour les données de plus de 42 000 octets, ou si les 1 à 5 pointeurs ne peuvent pas tenir en ligne, alors il n'y aura qu'un pointeur de 24 octets vers la page de départ d'une liste de pointeurs vers les pages LOB (c'est-à-dire le "text_tree "page).
- SI sp_tableoption a été utilisé pour définir l'
large value types out of row
option, utilisez toujours un pointeur de 16 octets vers le stockage LOB.
- J'ai dit des règles "par défaut" parce que je n'ai pas testé les valeurs en ligne contre l'impact de certaines fonctionnalités telles que la compression des données, le chiffrement au niveau des colonnes, le chiffrement transparent des données, toujours chiffré, etc.
Pages de débordement LOB: si une valeur est de 10 ko, cela nécessitera 1 page complète de 8 ko de débordement, puis une partie d'une 2e page. Si aucune autre donnée ne peut occuper l'espace restant (ou y est même autorisée, je ne suis pas sûr de cette règle), alors vous avez environ 6 Ko d'espace "gaspillé" sur cette deuxième page de débordement de LOB.
Espace inutilisé: Une page de données de 8k est juste cela: 8192 octets. Il ne varie pas en taille. Les données et métadonnées qui y sont placées, cependant, ne s'intègrent pas toujours bien dans les 8192 octets. Et les lignes ne peuvent pas être divisées sur plusieurs pages de données. Donc, s'il vous reste 100 octets mais qu'aucune ligne (ou aucune ligne qui pourrait tenir à cet emplacement, en fonction de plusieurs facteurs) ne peut y tenir, la page de données occupe toujours 8192 octets, et votre deuxième requête ne compte que le nombre de pages de données. Vous pouvez trouver cette valeur à deux endroits (gardez juste à l'esprit qu'une partie de cette valeur est une certaine quantité de cet espace réservé):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Recherchez = "EN-TÊTE DE ParentObject
PAGE:" et Field
= "m_freeCnt". Le Value
champ est le nombre d'octets inutilisés.
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;
Il s'agit de la même valeur que celle signalée par "m_freeCnt". C'est plus facile que DBCC car il peut obtenir de nombreuses pages, mais nécessite également que les pages aient été lues dans le pool de tampons en premier lieu.
Espace réservé par FILLFACTOR
<100. Les pages nouvellement créées ne respectent pas le FILLFACTOR
paramètre, mais effectuer une RECONSTRUCTION réservera cet espace sur chaque page de données. L'idée derrière l'espace réservé est qu'il sera utilisé par des insertions et / ou des mises à jour non séquentielles qui augmentent déjà la taille des lignes sur la page, en raison de la mise à jour des colonnes de longueur variable avec un peu plus de données (mais pas assez pour provoquer un page-split). Mais vous pouvez facilement réserver de l'espace sur des pages de données qui n'obtiendraient naturellement jamais de nouvelles lignes et n'auraient jamais les lignes existantes mises à jour, ou du moins pas mises à jour d'une manière qui augmenterait la taille de la ligne.
Page-Splits (fragmentation): La nécessité d'ajouter une ligne à un emplacement qui n'a pas de place pour la ligne entraînera une division de la page. Dans ce cas, environ 50% des données existantes sont déplacées vers une nouvelle page et la nouvelle ligne est ajoutée à l'une des 2 pages. Mais vous avez maintenant un peu plus d'espace libre qui n'est pas pris en compte par les DATALENGTH
calculs.
Lignes marquées pour suppression. Lorsque vous supprimez des lignes, elles ne sont pas toujours immédiatement supprimées de la page de données. S'ils ne peuvent pas être supprimés immédiatement, ils sont "marqués à mort" (référence Steven Segal) et seront physiquement supprimés plus tard par le processus de nettoyage des fantômes (je crois que c'est le nom). Cependant, ceux-ci pourraient ne pas être pertinents pour cette Question particulière.
Pages fantômes? Je ne sais pas si c'est le terme approprié, mais parfois les pages de données ne sont pas supprimées tant qu'une RECONSTRUCTION de l'index clusterisé n'est pas terminée. Cela représenterait également plus de pages qu'il n'en DATALENGTH
faudrait. Cela ne devrait généralement pas se produire, mais je l'ai rencontré une fois, il y a plusieurs années.
Colonnes SPARSE: les colonnes éparses économisent de l'espace (principalement pour les types de données de longueur fixe) dans les tables où un grand% des lignes sont NULL
pour une ou plusieurs colonnes. L' SPARSE
option fait NULL
monter le type de valeur de 0 octet (au lieu de la quantité de longueur fixe normale, comme 4 octets pour un INT
), mais les valeurs non NULL occupent chacune 4 octets supplémentaires pour les types de longueur fixe et une quantité variable pour types de longueur variable. Le problème ici est qu'il DATALENGTH
n'inclut pas les 4 octets supplémentaires pour les valeurs non NULL dans une colonne SPARSE, donc ces 4 octets doivent être ajoutés à nouveau. Vous pouvez vérifier s'il y a des SPARSE
colonnes via:
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;
Et puis pour chaque SPARSE
colonne, mettez à jour la requête d'origine pour utiliser:
SUM(DATALENGTH(FieldN) + 4)
Veuillez noter que le calcul ci-dessus pour ajouter 4 octets standard est un peu simpliste car il ne fonctionne que pour les types de longueur fixe. ET, il y a des métadonnées supplémentaires par ligne (d'après ce que je peux dire jusqu'à présent) qui réduisent l'espace disponible pour les données, simplement en ayant au moins une colonne SPARSE. Pour plus de détails, veuillez consulter la page MSDN pour Utiliser des colonnes éparses .
Index et autres pages (par exemple IAM, PFS, GAM, SGAM, etc.): ce ne sont pas des pages "données" en termes de données utilisateur. Ceux-ci gonfleront la taille totale de la table. Si vous utilisez SQL Server 2012 ou une version plus récente, vous pouvez utiliser la sys.dm_db_database_page_allocations
fonction de gestion dynamique (DMF) pour voir les types de page (les versions antérieures de SQL Server peuvent utiliser 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
Ni le DBCC IND
ni sys.dm_db_database_page_allocations
(avec cette clause WHERE) ne signalera aucune page d'index, et seul le DBCC IND
rapportera au moins une page IAM.
DATA_COMPRESSION: Si vous avez activé ROW
ou PAGE
Compression activé sur l'index cluster ou le tas, vous pouvez oublier la plupart de ce qui a été mentionné jusqu'à présent. L'en-tête de page de 96 octets, le tableau des emplacements de 2 octets par ligne et les informations de version de 14 octets par ligne sont toujours là, mais la représentation physique des données devient très complexe (beaucoup plus que ce qui a déjà été mentionné lors de la compression). n'est pas utilisé). Par exemple, avec la compression de lignes, SQL Server tente d'utiliser le plus petit conteneur possible pour s'adapter à chaque colonne, pour chaque ligne. Donc, si vous avez une BIGINT
colonne qui, autrement (en supposant qu'elle SPARSE
n'est pas également activée), prendrait toujours 8 octets, si la valeur est comprise entre -128 et 127 (c'est-à-dire un entier signé de 8 bits), elle n'utilisera qu'un seul octet, et si le la valeur pourrait tenir dans unSMALLINT
, il ne prendra que 2 octets. Les types entiers qui sont NULL
ou ne 0
prennent pas d'espace et sont simplement indiqués comme étant NULL
ou "vides" (c'est-à-dire 0
) dans un tableau mappant les colonnes. Et il y a beaucoup, beaucoup d'autres règles. Contiennent des données Unicode ( NCHAR
, NVARCHAR(1 - 4000)
mais pas NVARCHAR(MAX)
, même si elles sont stockées en ligne)? La compression Unicode a été ajoutée dans SQL Server 2008 R2, mais il n'y a aucun moyen de prédire le résultat de la valeur "compressée" dans toutes les situations sans effectuer la compression réelle étant donné la complexité des règles .
Donc, en réalité, votre deuxième requête, bien que plus précise en termes d'espace physique total occupé sur le disque, n'est vraiment précise qu'en effectuant un REBUILD
de l'index clusterisé. Et après cela, vous devez toujours tenir compte de tout FILLFACTOR
paramètre inférieur à 100. Et même dans ce cas, il y a toujours des en-têtes de page, et souvent suffisamment d'espace "gaspillé" qui n'est tout simplement pas remplissable car trop petit pour tenir dans une ligne de ce table, ou au moins la ligne qui devrait logiquement aller dans cet emplacement.
En ce qui concerne la précision de la 2e requête dans la détermination de l '"utilisation des données", il semble plus juste de sauvegarder les octets d'en-tête de page car ils ne sont pas des données: ils représentent des frais généraux liés au coût de l'entreprise. S'il y a 1 ligne sur une page de données et que cette ligne n'est qu'un TINYINT
, alors cet octet exigeait toujours que la page de données existe et donc les 96 octets de l'en-tête. Ce service devrait-il être facturé pour toute la page de données? Si cette page de données est ensuite remplie par le ministère # 2, répartiraient-ils également ces frais généraux ou paieraient-ils proportionnellement? Il semble plus facile de le retirer. Dans ce cas, l'utilisation d'une valeur de 8
pour se multiplier number of pages
est trop élevée. Que diriez-vous:
-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250
Par conséquent, utilisez quelque chose comme:
(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
pour tous les calculs par rapport aux colonnes "number_of_pages".
ET , considérant que l'utilisation DATALENGTH
par chaque champ ne peut pas renvoyer les métadonnées par ligne, qui doivent être ajoutées à votre requête par table où vous obtenez le DATALENGTH
par chaque champ, en filtrant sur chaque "département":
- Type d'enregistrement et décalage vers NULL Bitmap: 4 octets
- Nombre de colonnes: 2 octets
- Slot Array: 2 octets (non inclus dans la "taille d'enregistrement" mais doivent toujours être pris en compte)
- Bitmap NULL: 1 octet toutes les 8 colonnes (pour toutes les colonnes)
- Version de ligne: 14 octets (si la base de données a soit
ALLOW_SNAPSHOT_ISOLATION
ou est READ_COMMITTED_SNAPSHOT
définie sur ON
)
- Tableau de décalage de colonne de longueur variable: 0 octet si toutes les colonnes sont de longueur fixe. Si des colonnes sont de longueur variable, alors 2 octets, plus 2 octets pour chacune des colonnes de longueur variable uniquement.
- Pointeurs LOB: cette partie est très imprécise car il n'y aura pas de pointeur si la valeur est
NULL
, et si la valeur tient sur la ligne, elle peut être beaucoup plus petite ou beaucoup plus grande que le pointeur, et si la valeur est stockée hors- ligne, la taille du pointeur peut dépendre de la quantité de données. Cependant, puisque nous voulons juste une estimation (c'est-à-dire "swag"), il semble que 24 octets soit une bonne valeur à utiliser (enfin, aussi bonne que toute autre ;-). C'est pour chaque MAX
champ.
Par conséquent, utilisez quelque chose comme:
En général (en-tête de ligne + nombre de colonnes + tableau d'emplacements + bitmap NULL):
([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
En général (détection automatique si des "informations de version" sont présentes):
+ (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())
S'il existe des colonnes de longueur variable, ajoutez:
+ 2 + (2 * {NumVariableLengthColumns})
S'il y a des MAX
colonnes / LOB, ajoutez:
+ (24 * {NumLobColumns})
En général:
)) AS [MetaDataBytes]
Ce n'est pas exact, et encore une fois ne fonctionnera pas si vous avez activé la compression de ligne ou de page sur le tas ou l'index clusterisé, mais devrait certainement vous rapprocher.
MISE À JOUR concernant le mystère de la différence de 15%
Nous (moi-même inclus) étions tellement concentrés sur la façon dont les pages de données sont disposées et sur la façon dont DATALENGTH
pourraient expliquer les choses que nous n'avons pas passé beaucoup de temps à examiner la deuxième requête. J'ai exécuté cette requête sur une seule table, puis j'ai comparé ces valeurs à ce qui était signalé par sys.dm_db_database_page_allocations
et ce n'étaient pas les mêmes valeurs pour le nombre de pages. Sur une intuition, j'ai supprimé les fonctions d'agrégation et GROUP BY
, et remplacé la SELECT
liste par a.*, '---' AS [---], p.*
. Et puis, il est devenu clair: les gens doivent faire attention d'où sur ces interwebs troubles ils obtiennent leurs informations et leurs scripts ;-). La deuxième requête publiée dans la question n'est pas exactement correcte, en particulier pour cette question particulière.
Problème mineur: en dehors de cela n'a pas beaucoup de sens pour GROUP BY rows
(et ne pas avoir cette colonne dans une fonction d'agrégation), le JOIN entre sys.allocation_units
et sys.partitions
n'est pas techniquement correct. Il existe 3 types d'unités d'allocation, et l'un d'entre eux doit se joindre à un champ différent. Assez souvent partition_id
et hobt_id
sont les mêmes, donc il pourrait ne jamais y avoir de problème, mais parfois ces deux champs ont des valeurs différentes.
Problème majeur: la requête utilise le used_pages
champ. Ce champ couvre tous les types de pages: données, index, IAM, etc., tc. Il y a un autre, champ plus approprié à utiliser lorsque concerné que les données réelles: data_pages
.
J'ai adapté la 2e requête de la question en gardant à l'esprit les éléments ci-dessus et en utilisant la taille de la page de données qui sauvegarde l'en-tête de la page. J'ai aussi enlevé deux JOIN qui étaient inutiles: sys.schemas
(remplacé par appel SCHEMA_NAME()
), et sys.indexes
(l'index cluster est toujours index_id = 1
et nous avons index_id
en 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;