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.
I dati non sono l'unica cosa che occupa spazio su una pagina di dati 8k:
C'è spazio riservato. Puoi utilizzare solo 8060 degli 8192 byte (ovvero 132 byte che non sono mai stati tuoi in primo luogo):
- Intestazione di pagina: esattamente 96 byte.
- Matrice slot: si tratta di 2 byte per riga e indica l'offset del punto in cui ogni riga inizia sulla pagina. La dimensione di questo array non è limitata ai restanti 36 byte (132 - 96 = 36), altrimenti si sarebbe effettivamente limitati a mettere solo 18 righe al massimo in una pagina di dati. Ciò significa che ogni riga ha 2 byte in più di quanto si pensi. Questo valore non è incluso nella "dimensione del record" come riportato da
DBCC PAGE
, motivo per cui è tenuto separato qui invece di essere incluso nelle informazioni per riga di seguito.
- Meta-dati per riga (inclusi, ma non limitati a):
- Le dimensioni variano in base alla definizione della tabella (ad es. Numero di colonne, lunghezza variabile o lunghezza fissa, ecc.). Informazioni tratte dai commenti di @ PaulWhite e @ Aaron che possono essere trovati nella discussione relativa a questa risposta e test.
- Row-header: 4 byte, 2 dei quali indicano il tipo di record e gli altri due sono un offset rispetto alla bitmap NULL
- Numero di colonne: 2 byte
- NULL Bitmap: quali colonne sono attualmente
NULL
. 1 byte per ogni set di 8 colonne. E per tutte le colonne, anche NOT NULL
quelle. Quindi, minimo 1 byte.
- Matrice offset a colonna a lunghezza variabile: minimo 4 byte. 2 byte per contenere il numero di colonne di lunghezza variabile, quindi 2 byte per ogni colonna di lunghezza variabile per mantenere l'offset rispetto al punto di inizio.
- Informazioni sulla versione: 14 byte (sarà presente se il database è impostato su
ALLOW_SNAPSHOT_ISOLATION ON
o READ_COMMITTED_SNAPSHOT ON
).
- Per ulteriori dettagli, consultare la seguente domanda e risposta: Matrice di slot e Dimensione pagina totale
- Si prega di consultare il seguente post sul blog di Paul Randall che contiene diversi dettagli interessanti su come sono disposte le pagine di dati: Cercando con DBCC PAGE (Parte 1 di?)
Puntatori LOB per dati che non sono memorizzati in riga. Quindi ciò rappresenterebbe DATALENGTH
+ pointer_size. Ma questi non sono di dimensioni standard. Per i dettagli su questo argomento complesso, consultare il seguente post di blog: Qual è la dimensione del puntatore LOB per tipi (MAX) come Varchar, Varbinary, Etc? . Tra quel post collegato e alcuni test aggiuntivi che ho fatto , le regole (predefinite) dovrebbero essere le seguenti:
- Legacy / deprecato tipi LOB che nessuno dovrebbe usare più come di SQL Server 2005 (
TEXT
, NTEXT
, e IMAGE
):
- Per impostazione predefinita, archiviare sempre i propri dati su pagine LOB e utilizzare sempre un puntatore a 16 byte per l'archiviazione LOB.
- Se sp_tableoption è stato utilizzato per impostare l'
text in row
opzione, quindi:
- se nella pagina c'è spazio per memorizzare il valore e il valore non è maggiore della dimensione massima nella riga (intervallo configurabile di 24 - 7000 byte con un valore predefinito di 256), verrà archiviato nella riga,
- altrimenti sarà un puntatore a 16 byte.
- Per i tipi LOB più recenti introdotte in SQL Server 2005 (
VARCHAR(MAX)
, NVARCHAR(MAX)
, e VARBINARY(MAX)
):
- Di default:
- Se il valore non è maggiore di 8000 byte e c'è spazio nella pagina, verrà archiviato in riga.
- Radice incorporata: per i dati tra 8001 e 40.000 (in realtà 42.000) byte, spazio consentito, ci saranno da 1 a 5 puntatori (24 - 72 byte) IN RIGA che puntano direttamente alle pagine LOB. 24 byte per la pagina iniziale 8k LOB e 12 byte per ogni pagina aggiuntiva 8k per un massimo di altre quattro pagine 8k.
- TEXT_TREE - per i dati superiori a 42.000 byte o se i puntatori da 1 a 5 non possono adattarsi in fila, allora ci sarà solo un puntatore a 24 byte alla pagina iniziale di un elenco di puntatori alle pagine LOB (ovvero "text_tree " pagina).
- Se sp_tableoption è stato utilizzato per impostare l'
large value types out of row
opzione, quindi utilizzare sempre un puntatore a 16 byte per l'archiviazione LOB.
- Ho detto regole "predefinite" perché non ho testato i valori in fila contro l'impatto di alcune funzionalità come Compressione dati, Crittografia a livello di colonna, Crittografia dati trasparente, Crittografia sempre, ecc.
Pagine di overflow LOB: se un valore è 10k, ciò richiederà 1 pagina di overflow 8k completa e quindi parte di una seconda pagina. Se nessun altro dato può occupare lo spazio rimanente (o è anche permesso, non sono sicuro di quella regola), allora hai circa 6kb di spazio "sprecato" su quella seconda pagina di dati di overflow del LOB.
Spazio inutilizzato: una pagina di dati 8k è proprio questa: 8192 byte. Non varia di dimensioni. I dati e i metadati posti su di esso, tuttavia, non sempre si adattano perfettamente a tutti gli 8192 byte. E le righe non possono essere suddivise su più pagine di dati. Quindi, se hai ancora 100 byte ma nessuna riga (o nessuna riga che si adatterebbe in quella posizione, a seconda di diversi fattori) può adattarsi lì, la pagina di dati sta ancora occupando 8192 byte e la tua seconda query conta solo il numero di pagine di dati. Puoi trovare questo valore in due punti (tieni presente che una parte di questo valore è una quantità di quello spazio riservato):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Cerca ParentObject
= "PAGE HEADER:" e Field
= "m_freeCnt". Il Value
campo è il numero di byte non utilizzati.
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;
Questo è lo stesso valore riportato da "m_freeCnt". Questo è più semplice di DBCC poiché può ottenere molte pagine, ma richiede anche che le pagine siano state lette nel pool di buffer in primo luogo.
Spazio riservato da FILLFACTOR
<100. Le pagine appena create non rispettano l' FILLFACTOR
impostazione, ma facendo un REVISION si riserva lo spazio su ciascuna pagina di dati. L'idea alla base dello spazio riservato è che verrà utilizzato da inserimenti e / o aggiornamenti non sequenziali che espandono già la dimensione delle righe nella pagina, a causa dell'aggiornamento delle colonne a lunghezza variabile con un numero leggermente maggiore di dati (ma non abbastanza per causare un pagina-split). Ma potresti facilmente riservare spazio su pagine di dati che naturalmente non otterrebbero mai nuove righe e non avrebbero mai aggiornato le righe esistenti, o almeno non aggiornate in modo da aumentare le dimensioni della riga.
Divisioni di pagina (frammentazione): la necessità di aggiungere una riga in una posizione che non ha spazio per la riga causerà una divisione della pagina. In questo caso, circa il 50% dei dati esistenti viene spostato in una nuova pagina e la nuova riga viene aggiunta a una delle 2 pagine. Ma ora hai un po 'più di spazio libero che non è rappresentato dai DATALENGTH
calcoli.
Righe contrassegnate per l'eliminazione. Quando si eliminano le righe, queste non vengono sempre rimosse immediatamente dalla pagina dei dati. Se non possono essere rimossi immediatamente, vengono "contrassegnati per la morte" (riferimento a Steven Segal) e verranno rimossi fisicamente in seguito dal processo di pulizia dei fantasmi (credo che sia il nome). Tuttavia, questi potrebbero non essere rilevanti per questa particolare domanda.
Pagine fantasma? Non sono sicuro se questo sia il termine corretto, ma a volte le pagine di dati non vengono rimosse fino a quando non viene eseguita una RICOSTRUZIONE dell'indice cluster. Ciò spiegherebbe anche più pagine di quante DATALENGTH
ne aggiungerebbe. Questo in genere non dovrebbe accadere, ma l'ho incontrato una volta, diversi anni fa.
Colonne SPARSE: le colonne sparse risparmiano spazio (principalmente per tipi di dati a lunghezza fissa) nelle tabelle in cui una grande percentuale delle righe è NULL
per una o più colonne. L' SPARSE
opzione fa in modo che il NULL
valore comporti 0 byte (anziché la normale quantità a lunghezza fissa, come 4 byte per un INT
), ma i valori non NULL occupano ciascuno 4 byte aggiuntivi per tipi a lunghezza fissa e un importo variabile per tipi a lunghezza variabile. Il problema qui è che DATALENGTH
non include i 4 byte extra per i valori non NULL in una colonna SPARSE, quindi è necessario aggiungere nuovamente quei 4 byte. Puoi verificare se ci sono SPARSE
colonne tramite:
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;
E quindi per ogni SPARSE
colonna, aggiorna la query originale per utilizzare:
SUM(DATALENGTH(FieldN) + 4)
Si noti che il calcolo sopra riportato per aggiungere uno standard di 4 byte è un po 'semplicistico in quanto funziona solo per tipi a lunghezza fissa. E, ci sono ulteriori metadati per riga (da quello che posso dire finora) che riducono lo spazio disponibile per i dati, semplicemente avendo almeno una colonna SPARSE. Per ulteriori dettagli, consultare la pagina MSDN per Usa colonne sparse .
Indice e altre pagine (es. IAM, PFS, GAM, SGAM, ecc.): Queste non sono pagine di "dati" in termini di dati dell'utente. Questi aumenteranno la dimensione totale della tabella. Se si utilizza SQL Server 2012 o versioni successive, è possibile utilizzare la sys.dm_db_database_page_allocations
funzione di gestione dinamica (DMF) per visualizzare i tipi di pagina ( è possibile utilizzare le versioni precedenti di SQL Server 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
Né il DBCC IND
né sys.dm_db_database_page_allocations
(con quella clausola WHERE) segnalerà alcuna pagina dell'Indice e solo la DBCC IND
segnalerà almeno una pagina IAM.
DATA_COMPRESSION: se hai attivato ROW
o la PAGE
compressione sull'indice cluster o heap, puoi dimenticare la maggior parte di ciò che è stato menzionato finora. L'intestazione della pagina a 96 byte, l'array di slot da 2 byte per riga e le informazioni sulla versione di 14 byte per riga sono ancora presenti, ma la rappresentazione fisica dei dati diventa estremamente complessa (molto più di quanto è già stato menzionato durante la compressione non viene utilizzato). Ad esempio, con la compressione di riga, SQL Server tenta di utilizzare il contenitore più piccolo possibile per adattarsi a ciascuna colonna, per ogni riga. Quindi, se hai una BIGINT
colonna che altrimenti (supponendo che SPARSE
non sia abilitata) occuperebbe sempre 8 byte, se il valore è compreso tra -128 e 127 (cioè un intero a 8 bit con segno), utilizzerà solo 1 byte e se il il valore potrebbe rientrare in aSMALLINT
, richiederà solo 2 byte. Tipi interi che sono NULL
o 0
non occupano spazio e vengono semplicemente indicati come NULL
"vuoti" (ovvero 0
) in un array che traccia le colonne. E ci sono molte, molte altre regole. Hai dati Unicode ( NCHAR
, NVARCHAR(1 - 4000)
ma non NVARCHAR(MAX)
, anche se archiviati in fila)? La compressione Unicode è stata aggiunta in SQL Server 2008 R2, ma non è possibile prevedere l'esito del valore "compresso" in tutte le situazioni senza eseguire la compressione effettiva data la complessità delle regole .
Quindi, davvero, la tua seconda query, sebbene più accurata in termini di spazio fisico totale occupato su disco, è davvero accurata solo quando si esegue un REBUILD
indice cluster. FILLFACTOR
Dopodiché , devi comunque tenere conto di qualsiasi impostazione inferiore a 100. E anche allora ci sono sempre intestazioni di pagina, e spesso abbastanza quantità di spazio "sprecato" che non è semplicemente compilabile a causa dell'essere troppo piccolo per adattarsi a qualsiasi riga in questo tabella, o almeno la riga che logicamente dovrebbe andare in quello slot.
Per quanto riguarda l'accuratezza della seconda query nel determinare "utilizzo dei dati", sembra più corretto eseguire il back-out dei byte dell'intestazione della pagina poiché non sono un utilizzo dei dati: sono costi generali di business. Se c'è una riga su una pagina di dati e quella riga è solo una TINYINT
, allora quel 1 byte richiede comunque che la pagina di dati esista e quindi i 96 byte dell'intestazione. Quel reparto dovrebbe essere addebitato per l'intera pagina di dati? Se la pagina dei dati fosse quindi riempita dal Dipartimento n. 2, dividerebbero uniformemente quel costo "generale" o pagherebbero in modo proporzionale? Sembra più semplice semplicemente ripristinarlo. In tal caso, l'utilizzo di un valore 8
da moltiplicare per number of pages
è troppo alto. Che ne dite di:
-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250
Quindi, usa qualcosa come:
(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
per tutti i calcoli rispetto alle colonne "number_of_pages".
E , considerando che l'utilizzo DATALENGTH
per ciascun campo non può restituire i metadati per riga, che dovrebbe essere aggiunto alla query per tabella in cui si ottiene il DATALENGTH
campo per ciascun campo, filtrando su ciascun "reparto":
- Tipo di record e offset su NULL Bitmap: 4 byte
- Numero di colonne: 2 byte
- Matrice di slot: 2 byte (non inclusa nella "dimensione del record" ma deve comunque essere considerata)
- Bitmap NULL: 1 byte ogni 8 colonne (per tutte le colonne)
- Row Versioning: 14 byte (se il database ha
ALLOW_SNAPSHOT_ISOLATION
o READ_COMMITTED_SNAPSHOT
impostato su ON
)
- Matrice offset a colonna a lunghezza variabile: 0 byte se tutte le colonne sono a lunghezza fissa. Se le colonne sono di lunghezza variabile, quindi 2 byte, più 2 byte per ciascuna delle sole colonne di lunghezza variabile.
- Puntatori LOB: questa parte è molto imprecisa poiché non ci sarà un puntatore se il valore è
NULL
, e se il valore si adatta alla riga, allora può essere molto più piccolo o molto più grande del puntatore e se il valore è archiviato off- riga, quindi la dimensione del puntatore potrebbe dipendere dalla quantità di dati presenti. Tuttavia, poiché vogliamo solo una stima (ovvero "swag"), sembra che 24 byte sia un buon valore da usare (beh, buono come qualsiasi altro ;-). Questo è per ogni MAX
campo.
Quindi, usa qualcosa come:
In generale (intestazione di riga + numero di colonne + array di slot + bitmap NULL):
([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
In generale (rileva automaticamente se sono presenti "informazioni sulla versione"):
+ (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())
SE ci sono colonne di lunghezza variabile, quindi aggiungi:
+ 2 + (2 * {NumVariableLengthColumns})
SE ci sono MAX
colonne / LOB, quindi aggiungi:
+ (24 * {NumLobColumns})
In generale:
)) AS [MetaDataBytes]
Ciò non è esatto e, di nuovo, non funzionerà se è stata abilitata la compressione di riga o di pagina nell'heap o nell'indice cluster, ma dovrebbe sicuramente avvicinarti.
AGGIORNAMENTO per quanto riguarda il mistero delle differenze del 15%
Noi (incluso me stesso) eravamo così concentrati nel pensare a come sono disposte le pagine di dati e su come DATALENGTH
potrebbero spiegare cose che non abbiamo trascorso molto tempo a rivedere la seconda query. Ho eseguito quella query su una singola tabella e quindi ho confrontato quei valori con ciò che veniva segnalato sys.dm_db_database_page_allocations
e non erano gli stessi valori per il numero di pagine. In un sospetto, ho rimosso le funzioni di aggregazione e ho GROUP BY
sostituito l' SELECT
elenco con a.*, '---' AS [---], p.*
. E poi è diventato chiaro: le persone devono stare attenti da dove su queste oscure interwebs ottengono le loro informazioni e script ;-). La seconda query pubblicata nella domanda non è esattamente corretta, specialmente per questa particolare domanda.
Problema minore: al di fuori di esso non ha molto senso GROUP BY rows
(e non ha quella colonna in una funzione aggregata), il JOIN tra sys.allocation_units
e sys.partitions
non è tecnicamente corretto. Esistono 3 tipi di unità di allocazione e una di esse dovrebbe ISCRIVERSI a un altro campo. Abbastanza spesso partition_id
e hobt_id
sono uguali, quindi potrebbe non esserci mai un problema, ma a volte quei due campi hanno valori diversi.
Problema principale: la query utilizza il used_pages
campo. Quel campo copre tutti i tipi di pagine: dati, indice, IAM, ecc., Tc. V'è un altro, il campo più appropriato da utilizzare quando si occupa di solo i dati effettivi: data_pages
.
Ho adattato la seconda query nella domanda tenendo presente gli elementi precedenti e utilizzando la dimensione della pagina di dati che esegue il backout dell'intestazione della pagina. Ho anche rimosso due join che erano inutili: sys.schemas
(sostituito con chiamata a SCHEMA_NAME()
), e sys.indexes
(l'indice cluster è sempre index_id = 1
e noi abbiamo 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;