512 octets ne sont pas utilisés à partir de la page de données de 8 Ko de SQL Server

13

J'ai créé le tableau suivant:

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

puis créé un index clusterisé:

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

Ensuite, je l'ai rempli avec 30 lignes, chaque taille est de 256 octets (basée sur la déclaration de la table):

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;

Maintenant basé sur les informations que j'ai lues dans le livre "Kit de formation (examen 70-461): Interrogation de Microsoft SQL Server 2012 (Itzik Ben-Gan)":

SQL Server organise en interne les données dans un fichier de données en pages. Une page est une unité de 8 Ko et appartient à un seul objet; par exemple, vers une table ou un index. Une page est la plus petite unité de lecture et d'écriture. Les pages sont en outre organisées en étendues. Une étendue se compose de huit pages consécutives. Les pages d'une étendue peuvent appartenir à un seul objet ou à plusieurs objets. Si les pages appartiennent à plusieurs objets, l'extension est appelée extension mixte; si les pages appartiennent à un seul objet, alors l'étendue est appelée une étendue uniforme. SQL Server stocke les huit premières pages d'un objet dans des extensions mixtes. Lorsqu'un objet dépasse huit pages, SQL Server alloue des extensions uniformes supplémentaires pour cet objet. Avec cette organisation, les petits objets perdent moins d'espace et les gros objets sont moins fragmentés.

J'ai donc ici la première page d'étendue mixte de 8 Ko, remplie de 7680 octets (j'ai inséré 30 fois la ligne de taille de 256 octets, donc 30 * 256 = 7680), pour vérifier la taille, j'ai exécuté la vérification de la taille proc - elle renvoie le résultat suivant

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

Donc 16 Ko sont réservés pour la table, la première page de 8 Ko est pour la page Root IAM, la seconde est pour la page de stockage de données feuille qui est de 8 Ko avec une occupation de ~ 7,5 Ko, maintenant lorsque j'insère une nouvelle ligne avec 256 octets:

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

il n'est pas stocké dans la même page bien qu'il ait un espace de 256 octets (7680 b + 256 = 7936 qui est encore plus petit que 8 Ko), une nouvelle page de données est créée, mais cette nouvelle ligne pourrait tenir sur la même ancienne page , pourquoi SQL Server crée-t-il une nouvelle page alors qu'il peut économiser de l'espace et du temps de recherche en l'insérant dans la page existante?

Remarque: la même chose se produit dans l'index de tas.

Alphas Supremum
la source

Réponses:

9

Vos lignes de données ne font pas 256 octets. Chacun ressemble plus à 263 octets. Une ligne de données de types de données de longueur purement fixe a une surcharge supplémentaire en raison de la structure d'une ligne de données dans SQL Server. Jetez un œil à ce site et découvrez comment une ligne de données est constituée. http://aboutsqlserver.com/2013/10/15/sql-server-storage-engine-data-pages-and-data-rows/

Donc, dans votre exemple, vous avez une ligne de données qui a 256 octets, ajoutez 2 octets pour les bits d'état, 2 octets pour le nombre de colonnes, 2 octets pour la longueur des données et encore 1 ou deux pour le bitmap nul. C'est 263 * 30 = 7 890 octets. Ajoutez un autre 263 et vous êtes au-dessus de la limite de 8 Ko, ce qui forcerait la création d'une autre page.

dfundako
la source
Le lien que vous avez fourni m'a aidé à mieux visualiser la structure de la page, je cherchais quelque chose de similaire mais je n'ai pas pu le trouver, Thax
Alphas Supremum
11

S'il est vrai que SQL Server utilise des pages de données de 8k (8192 octets) pour stocker 1 ou plusieurs lignes, chaque page de données a une surcharge (96 octets) et chaque ligne a une surcharge (au moins 9 octets). Les 8192 octets ne sont pas uniquement des données.

Pour un examen plus détaillé de la façon dont cela fonctionne, veuillez consulter ma réponse à la question DBA.SE suivante:

SUM des DATALENGTH ne correspondant pas à la taille de la table de sys.allocation_units

En utilisant les informations de cette réponse liée, nous pouvons obtenir une image plus claire de la taille réelle de la ligne:

  1. En-tête de ligne = 4 octets
  2. Nombre de colonnes = 2 octets
  3. Bitmap NULL = 1 octet
  4. Informations sur la version ** = 14 octets (facultatif, voir note de bas de page)
  5. Surcharge totale par ligne (hors baie de logements) = 7 octets minimum, ou 21 octets si les informations de version sont présentes
  6. Taille de ligne réelle totale = 263 minimum (256 données + 7 frais généraux), ou 277 octets (256 données + 21 frais généraux) si les informations de version sont présentes
  7. En ajoutant le Slot Array, l'espace total pris par ligne est en fait de 265 octets (sans informations de version) ou de 279 octets (avec informations de version).

L'utilisation DBCC PAGEconfirme mon calcul en affichant: Record Size 263(pour tempdb) et Record Size 277(pour une base de données définie sur ALLOW_SNAPSHOT_ISOLATION ON).

Maintenant, avec 30 lignes, c'est:

  • SANS info de version

    30 * 263 nous donnerait 7890 octets. Ajoutez ensuite les 96 octets d'en-tête de page pour 7986 octets utilisés. Enfin, ajoutez les 60 octets (2 par ligne) de la baie de logements pour un total de 8046 octets utilisés sur la page et 146 restants. L'utilisation DBCC PAGEconfirme mon calcul en montrant:

    • m_slotCnt 30 (ie nombre de lignes)
    • m_freeCnt 146 (c'est-à-dire le nombre d'octets restants sur la page)
    • m_freeData 7986 (c.-à-d. données + en-tête de page - 7890 + 96 - le tableau des emplacements n'est pas pris en compte dans le calcul des octets "utilisés")
  • AVEC les informations de version

    30 * 277 octets pour un total de 8310 octets. Mais 8310 est supérieur à 8192, et cela ne tenait même pas compte de l'en-tête de page de 96 octets ni du tableau d'emplacements de 2 octets par ligne (30 * 2 = 60 octets), ce qui ne devrait nous donner que 8036 octets utilisables pour les lignes.

    MAIS, qu'en est-il de 29 lignes? Cela nous donnerait 8033 octets de données (29 * 277) + 96 octets pour l'en-tête de page + 58 octets pour la baie de slots (29 * 2) soit 8187 octets. Et cela laisserait la page avec 5 octets restants (8192 - 8187; inutilisable, bien sûr). L'utilisation DBCC PAGEconfirme mon calcul en montrant:

    • m_slotCnt 29 (ie nombre de lignes)
    • m_freeCnt 5 (c'est-à-dire le nombre d'octets restants sur la page)
    • m_freeData 8129 (c'est-à-dire données + en-tête de page - 8033 + 96 - le tableau des emplacements n'est pas pris en compte dans le calcul des octets "utilisés")

Concernant les tas

Les tas remplissent les pages de données légèrement différemment. Ils conservent une estimation très approximative de la quantité d'espace restant sur la page. Lorsque l'on regarde à la sortie de DBCC, regardez la ligne pour: PAGE HEADER: Allocation Status PFS (1:1). Vous verrez le VALUEmontrant quelque chose le long des lignes de 0x60 MIXED_EXT ALLOCATED 0_PCT_FULL(quand j'ai regardé la table Clustered) ou 0x64 MIXED_EXT ALLOCATED 100_PCT_FULLen regardant la table Heap. Ceci est évalué par transaction, donc faire des insertions individuelles telles que le test effectué ici pourrait afficher des résultats différents entre les tables en cluster et les tables de tas. Cependant, effectuer une seule opération DML pour les 30 lignes remplira le tas comme prévu.

Cependant, aucun de ces détails concernant les tas n'affecte directement ce test particulier car les deux versions du tableau tiennent sur 30 lignes avec seulement 146 octets restants. Ce n'est pas assez d'espace pour une autre ligne, indépendamment de Clustered ou Heap.

N'oubliez pas que ce test est assez simple. Le calcul de la taille réelle d'une ligne peut devenir très compliqué en fonction de divers facteurs, tels que SPARSE:, compression des données, données LOB, etc.


Pour voir les détails de la page de données, utilisez la requête suivante:

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;

** La valeur "version info" de 14 octets sera présente si votre base de données est définie sur ALLOW_SNAPSHOT_ISOLATION ONou READ_COMMITTED_SNAPSHOT ON.

Solomon Rutzky
la source
Pour être précis, 8060 octets par page sont disponibles pour les données utilisateur. Les données du PO sont toujours inférieures à cela.
Roger Wolf
Les informations de version ne sont pas là, sinon 30 lignes prendraient 8310 octets. Le reste semble correct.
Roger Wolf
@RogerWolf Oui, les "informations de version" sont là. Et donc oui, 30 lignes nécessitent 8310 octets. C'est pourquoi ces 30 lignes ne correspondaient pas en fait à une seule page, car l'OP est amené à croire, quel que soit le processus de test utilisé par l'OP. Mais ce test est faux. Seules 29 lignes tiennent sur la page. J'ai confirmé cela (en utilisant même SQL Server 2012).
Solomon Rutzky
avez-vous essayé d'exécuter votre test sur une base de données qui n'est pas compatible RCSI / tempdb? J'ai pu reproduire les chiffres exacts fournis par OP.
Roger Wolf
@RogerWolf La base de données que j'utilise n'est pas compatible RCSI, mais elle est définie sur ALLOW_SNAPSHOT_ISOLATION. J'ai aussi juste essayé tempdbet vu que les "informations sur la version" ne sont pas là, donc 30 rangées conviennent. Je mettrai à jour pour ajouter les nouvelles informations.
Solomon Rutzky
3

La structure réelle de la page de données est assez complexe. Bien qu'il soit généralement indiqué que 8060 octets par page sont disponibles pour les données utilisateur, il existe une surcharge supplémentaire non comptée nulle part qui entraîne ce comportement.

Cependant, vous avez peut-être remarqué que SQL Server vous donne en fait un indice que la 31e ligne ne rentrera pas dans la page. Pour que la ligne suivante tienne sur la même page, la avg_page_space_used_in_percentvaleur doit être inférieure à 100% - (100/31) = 96,774194, et dans votre cas, c'est bien au-dessus.

PS Je crois avoir vu une explication détaillée, jusqu'à l'octet de la structure de la page de données dans l'un des livres "SQL Server Internals" de Kalen Delaney, mais c'était il y a presque 10 ans, alors veuillez m'excuser de ne pas me souvenir de plus de détails. En outre, la structure des pages a tendance à changer d'une version à l'autre.

Roger Wolf
la source
1
Non. L'uniquificateur n'est ajouté qu'aux lignes de clés en double. La première ligne de chaque valeur de clé unique n'inclut pas l'uniquificateur supplémentaire de 4 octets.
Solomon Rutzky
@srutzky, apparemment vous avez raison. Je n'ai jamais pensé que SQL Server autoriserait des clés de largeur variable. C'est moche. Efficace, oui, mais moche.
Roger Wolf