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.
Los datos no son lo único que ocupa espacio en una página de datos de 8k:
Hay espacio reservado. Solo puede usar 8060 de los 8192 bytes (eso es 132 bytes que nunca fueron suyos en primer lugar):
- Encabezado de página: esto es exactamente 96 bytes.
- Matriz de ranuras: esto es 2 bytes por fila e indica el desplazamiento de dónde comienza cada fila en la página. El tamaño de esta matriz no se limita a los 36 bytes restantes (132 - 96 = 36), de lo contrario, se limitaría efectivamente a poner solo 18 filas como máximo en una página de datos. Esto significa que cada fila es 2 bytes más grande de lo que crees. Este valor no se incluye en el "tamaño de registro" según lo informado por
DBCC PAGE
, por lo que se mantiene separado aquí en lugar de incluirse en la información por fila a continuación.
- Metadatos por fila (incluidos, entre otros):
- El tamaño varía según la definición de la tabla (es decir, número de columnas, longitud variable o longitud fija, etc.). Información tomada de los comentarios de @ PaulWhite y @ Aaron que se pueden encontrar en la discusión relacionada con esta respuesta y prueba.
- Encabezado de fila: 4 bytes, 2 de ellos denotan el tipo de registro y los otros dos son un desplazamiento del mapa de bits NULL
- Número de columnas: 2 bytes.
- Mapa de bits NULL: qué columnas están actualmente
NULL
. 1 byte por cada conjunto de 8 columnas. Y para todas las columnas, incluso NOT NULL
las. Por lo tanto, mínimo 1 byte.
- Matriz de desplazamiento de columna de longitud variable: 4 bytes como mínimo. 2 bytes para contener el número de columnas de longitud variable, y luego 2 bytes por cada columna de longitud variable para mantener el desplazamiento donde comienza.
- Información de versiones: 14 bytes (esto estará presente si su base de datos está configurada en
ALLOW_SNAPSHOT_ISOLATION ON
o READ_COMMITTED_SNAPSHOT ON
).
- Consulte la siguiente pregunta y respuesta para obtener más detalles al respecto: matriz de ranuras y tamaño total de página
- Consulte la siguiente publicación de blog de Paul Randall que tiene varios detalles interesantes sobre cómo se presentan las páginas de datos: hurgando con la página DBCC (Parte 1 de?)
Punteros LOB para datos que no están almacenados en fila. Entonces eso explicaría DATALENGTH
+ pointer_size. Pero estos no son de un tamaño estándar. Consulte la siguiente publicación de blog para obtener detalles sobre este tema complejo: ¿Cuál es el tamaño del puntero LOB para tipos (MAX) como Varchar, Varbinary, Etc? . Entre esa publicación vinculada y algunas pruebas adicionales que he realizado , las reglas (predeterminadas) deberían ser las siguientes:
- Legacy / desaprobado tipos LOB que nadie debería usar más como de SQL Server 2005 (
TEXT
, NTEXT
y IMAGE
):
- Por defecto, siempre almacene sus datos en páginas LOB y siempre use un puntero de 16 bytes para el almacenamiento LOB.
- SI se usó sp_tableoption para establecer la
text in row
opción, entonces:
- si hay espacio en la página para almacenar el valor, y el valor no es mayor que el tamaño máximo de fila (rango configurable de 24 - 7000 bytes con un valor predeterminado de 256), entonces se almacenará en fila,
- de lo contrario, será un puntero de 16 bytes.
- Para los tipos LOB más recientes introducidas en SQL Server 2005 (
VARCHAR(MAX)
, NVARCHAR(MAX)
y VARBINARY(MAX)
):
- Por defecto:
- Si el valor no es superior a 8000 bytes y hay espacio en la página, se almacenará en fila.
- Raíz en línea: para datos entre 8001 y 40,000 (realmente 42,000) bytes, si el espacio lo permite, habrá de 1 a 5 punteros (24 - 72 bytes) EN FILA que apuntan directamente a la (s) página (s) LOB. 24 bytes para la página inicial de 8k LOB, y 12 bytes por cada página adicional de 8k para hasta cuatro páginas de 8k más.
- TEXT_TREE: para datos de más de 42,000 bytes, o si los punteros de 1 a 5 no caben en la fila, entonces solo habrá un puntero de 24 bytes a la página de inicio de una lista de punteros a las páginas LOB (es decir, "text_tree " página).
- SI se usó sp_tableoption para establecer la
large value types out of row
opción, siempre use un puntero de 16 bytes para almacenamiento LOB.
- Dije reglas "por defecto", porque Yo no probar valores en fila contra el impacto de ciertas características tales como la compresión de datos, encriptación a nivel de columna, Transparente cifrado de datos, siempre cifrados, etc.
Páginas de desbordamiento de LOB: si un valor es 10k, entonces eso requerirá 1 página de desbordamiento de 8k completa, y luego parte de una segunda página. Si ningún otro dato puede ocupar el espacio restante (o incluso se permite, no estoy seguro de esa regla), entonces tiene aproximadamente 6 kb de espacio "desperdiciado" en esa segunda página de datos de desbordamiento de LOB.
Espacio no utilizado: una página de datos de 8k es solo eso: 8192 bytes. No varía en tamaño. Sin embargo, los datos y metadatos que se le asignan no siempre encajan bien en todos los 8192 bytes. Y las filas no se pueden dividir en varias páginas de datos. Por lo tanto, si tiene 100 bytes restantes pero ninguna fila (o ninguna fila que cabría en esa ubicación, dependiendo de varios factores) puede caber allí, la página de datos aún ocupa 8192 bytes, y su segunda consulta solo cuenta el número de páginas de datos Puede encontrar este valor en dos lugares (solo tenga en cuenta que una parte de este valor es una cantidad de ese espacio reservado):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Busque ParentObject
= "ENCABEZADO DE PÁGINA:" y Field
= "m_freeCnt". El Value
campo es el número de bytes no utilizados.
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;
Este es el mismo valor reportado por "m_freeCnt". Esto es más fácil que DBCC ya que puede obtener muchas páginas, pero también requiere que las páginas se hayan leído en el grupo de búferes en primer lugar.
Espacio reservado por FILLFACTOR
<100. Las páginas recién creadas no respetan la FILLFACTOR
configuración, pero al realizar una RECONSTRUCCIÓN se reservará ese espacio en cada página de datos. La idea detrás del espacio reservado es que será utilizado por inserciones no secuenciales y / o actualizaciones que ya expanden el tamaño de las filas en la página, debido a que las columnas de longitud variable se actualizan con un poco más de datos (pero no lo suficiente como para causar un división de página). Pero podría reservar fácilmente espacio en páginas de datos que, naturalmente, nunca obtendrían nuevas filas y nunca actualizarían las filas existentes, o al menos no se actualizarían de una manera que aumentaría el tamaño de la fila.
División de página (fragmentación): la necesidad de agregar una fila a una ubicación que no tiene espacio para la fila provocará una división de página. En este caso, aproximadamente el 50% de los datos existentes se mueven a una nueva página y la nueva fila se agrega a una de las 2 páginas. Pero ahora tiene un poco más de espacio libre que no se tiene en cuenta en los DATALENGTH
cálculos.
Filas marcadas para su eliminación. Cuando elimina filas, no siempre se eliminan inmediatamente de la página de datos. Si no pueden eliminarse de inmediato, están "marcados para la muerte" (referencia de Steven Segal) y serán eliminados físicamente más tarde por el proceso de limpieza de fantasmas (creo que ese es el nombre). Sin embargo, estos podrían no ser relevantes para esta pregunta en particular.
Páginas fantasma? No estoy seguro de si ese es el término apropiado, pero a veces las páginas de datos no se eliminan hasta que se realiza una RECONSTRUCCIÓN del índice agrupado. Eso también representaría más páginas de las DATALENGTH
que sumaría. Esto generalmente no debería suceder, pero me he encontrado con él una vez, hace varios años.
Columnas SPARSE: las columnas dispersas ahorran espacio (principalmente para tipos de datos de longitud fija) en tablas donde un gran% de las filas son NULL
para una o más columnas. La SPARSE
opción hace que el NULL
tipo de valor aumente 0 bytes (en lugar de la cantidad normal de longitud fija, como 4 bytes para un INT
), pero los valores no NULL ocupan 4 bytes adicionales para los tipos de longitud fija y una cantidad variable para tipos de longitud variable. El problema aquí es que DATALENGTH
no incluye los 4 bytes adicionales para valores no NULL en una columna SPARSE, por lo que esos 4 bytes deben agregarse nuevamente. Puede verificar si hay SPARSE
columnas a través de:
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;
Y luego, para cada SPARSE
columna, actualice la consulta original para usar:
SUM(DATALENGTH(FieldN) + 4)
Tenga en cuenta que el cálculo anterior para agregar 4 bytes estándar es un poco simplista, ya que solo funciona para tipos de longitud fija. Y, hay metadatos adicionales por fila (de lo que puedo decir hasta ahora) que reduce el espacio disponible para los datos, simplemente al tener al menos una columna SPARSE. Para obtener más detalles, consulte la página de MSDN para Usar columnas dispersas .
Índice y otras páginas (por ejemplo, IAM, PFS, GAM, SGAM, etc.): estas no son páginas de "datos" en términos de datos del usuario. Estos inflarán el tamaño total de la tabla. Si usa SQL Server 2012 o posterior, puede usar la sys.dm_db_database_page_allocations
Función de administración dinámica (DMF) para ver los tipos de página (pueden usar versiones anteriores de 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
Ni el DBCC IND
ni sys.dm_db_database_page_allocations
(con esa cláusula WHERE) informará ninguna página de índice, y solo el DBCC IND
informará al menos una página IAM.
DATA_COMPRESSION: si tiene habilitado ROW
o PAGE
Compresión en el índice agrupado o el montón, puede olvidarse de la mayoría de lo que se ha mencionado hasta ahora. El encabezado de página de 96 bytes, la matriz de ranuras de 2 bytes por fila y la información de versiones de 14 bytes por fila todavía están allí, pero la representación física de los datos se vuelve muy compleja (mucho más de lo que ya se ha mencionado cuando Compression no se está utilizando) Por ejemplo, con la compresión de filas, SQL Server intenta usar el contenedor más pequeño posible para ajustar cada columna, por cada fila. Entonces, si tiene una BIGINT
columna que de lo contrario (suponiendo SPARSE
que tampoco esté habilitada) siempre ocupará 8 bytes, si el valor está entre -128 y 127 (es decir, entero de 8 bits con signo), usará solo 1 byte, y si el valor podría caber en unSMALLINT
, solo ocupará 2 bytes. Los tipos enteros que son NULL
o 0
no ocupan espacio y simplemente se indican como estar NULL
o "vacíos" (es decir 0
) en una matriz que asigna las columnas. Y hay muchas, muchas otras reglas. Los datos han Unicode ( NCHAR
, NVARCHAR(1 - 4000)
pero no NVARCHAR(MAX)
, incluso si se almacena en fila)? La compresión Unicode se agregó en SQL Server 2008 R2, pero no hay forma de predecir el resultado del valor "comprimido" en todas las situaciones sin realizar la compresión real dada la complejidad de las reglas .
Entonces, realmente, su segunda consulta, aunque es más precisa en términos del espacio físico total ocupado en el disco, solo es realmente precisa al hacer un REBUILD
índice agrupado. Y después de eso, aún debe tener en cuenta cualquier FILLFACTOR
configuración por debajo de 100. E incluso entonces siempre hay encabezados de página y, a menudo, una cantidad suficiente de espacio "desperdiciado" que simplemente no se puede llenar debido a que es demasiado pequeño para caber en cualquier fila en este tabla, o al menos la fila que lógicamente debería ir en esa ranura.
Con respecto a la precisión de la segunda consulta para determinar el "uso de datos", parece más justo anular los bytes del encabezado de página, ya que no son el uso de datos: son gastos generales del costo del negocio. Si hay 1 fila en una página de datos y esa fila es solo un TINYINT
, entonces ese 1 byte aún requiere que la página de datos exista y, por lo tanto, los 96 bytes del encabezado. ¿Se debe cobrar a ese departamento por toda la página de datos? Si esa página de datos la llena el Departamento # 2, ¿dividirían equitativamente ese costo "general" o pagarían proporcionalmente? Parece más fácil simplemente retroceder. En cuyo caso, usar un valor de 8
para multiplicar number of pages
es demasiado alto. Qué tal si:
-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250
Por lo tanto, use algo como:
(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
para todos los cálculos contra columnas "número_de_páginas".
Y , teniendo en cuenta que el uso DATALENGTH
por cada campo no puede devolver los metadatos por fila, eso debe agregarse a su consulta por tabla donde obtiene el DATALENGTH
por cada campo, filtrando en cada "departamento":
- Tipo de registro y desplazamiento a mapa de bits NULL: 4 bytes
- Recuento de columnas: 2 bytes
- Matriz de ranuras: 2 bytes (no incluido en el "tamaño de registro", pero aún debe tener en cuenta)
- Mapa de bits NULL: 1 byte por cada 8 columnas (para todas las columnas)
- Control de versiones de fila: 14 bytes (si la base de datos tiene
ALLOW_SNAPSHOT_ISOLATION
o está READ_COMMITTED_SNAPSHOT
establecida en ON
)
- Matriz de desplazamiento de columna de longitud variable: 0 bytes si todas las columnas son de longitud fija. Si alguna columna es de longitud variable, entonces 2 bytes, más 2 bytes por cada una de las columnas de longitud variable.
- Punteros LOB: esta parte es muy imprecisa ya que no habrá un puntero si el valor es
NULL
, y si el valor cabe en la fila, entonces puede ser mucho más pequeño o mucho más grande que el puntero, y si el valor se almacena fuera de fila, entonces el tamaño del puntero puede depender de la cantidad de datos que haya. Sin embargo, dado que solo queremos una estimación (es decir, "swag"), parece que 24 bytes es un buen valor para usar (bueno, tan bueno como cualquier otro ;-). Esto es por cada MAX
campo.
Por lo tanto, use algo como:
En general (encabezado de fila + número de columnas + matriz de ranuras + mapa de bits NULL):
([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
En general (detección automática si hay "información de versión"):
+ (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())
SI hay columnas de longitud variable, agregue:
+ 2 + (2 * {NumVariableLengthColumns})
SI hay MAX
columnas / LOB, luego agregue:
+ (24 * {NumLobColumns})
En general:
)) AS [MetaDataBytes]
Esto no es exacto, y nuevamente no funcionará si tiene habilitada la compresión de filas o páginas en el montón o el índice agrupado, pero definitivamente debería acercarlo.
ACTUALIZACIÓN sobre el misterio del 15% de diferencia
Nosotros (incluido yo mismo) estábamos tan centrados en pensar en cómo se distribuyen las páginas de datos y cómo DATALENGTH
podrían explicar las cosas que no pasamos mucho tiempo revisando la segunda consulta. Ejecuté esa consulta en una sola tabla y luego comparé esos valores con lo que informaba sys.dm_db_database_page_allocations
y no eran los mismos valores para el número de páginas. En una corazonada, eliminé las funciones agregadas GROUP BY
y reemplacé la SELECT
lista con a.*, '---' AS [---], p.*
. Y luego quedó claro: las personas deben tener cuidado de dónde obtienen información y guiones de estas interwebs turbias ;-). La segunda consulta publicada en la pregunta no es exactamente correcta, especialmente para esta pregunta en particular.
Problema menor: fuera de él no tiene mucho sentido GROUP BY rows
(y no tener esa columna en una función agregada), la UNIÓN entre sys.allocation_units
y sys.partitions
no es técnicamente correcta. Hay 3 tipos de unidades de asignación, y una de ellas debería UNIRSE a un campo diferente. Muy a menudo partition_id
y hobt_id
son lo mismo, por lo que puede que nunca haya un problema, pero a veces esos dos campos tienen valores diferentes.
Problema principal: la consulta usa el used_pages
campo. Ese campo cubre todos los tipos de páginas: Datos, Índice, IAM, etc., tc. Hay otro, el campo más adecuado para su uso cuando se trate sólo con los datos reales: data_pages
.
Adapte la segunda consulta en la Pregunta con los elementos anteriores en mente, y usando el tamaño de página de datos que retrocede el encabezado de la página. También he eliminado dos combinaciones que eran innecesarios: sys.schemas
(reemplazado con llamada a SCHEMA_NAME()
) y sys.indexes
(el índice agrupado es siempre index_id = 1
y tenemos 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;