Tenía la impresión de que si tuviera que sumar DATALENGTH()
todos los campos para todos los registros en una tabla, obtendría el tamaño total de la tabla. ¿Estoy equivocado?
SELECT
SUM(DATALENGTH(Field1)) +
SUM(DATALENGTH(Field2)) +
SUM(DATALENGTH(Field3)) TotalSizeInBytes
FROM SomeTable
WHERE X, Y, and Z are true
Utilicé esta consulta a continuación (que obtuve en línea para obtener tamaños de tabla, índices agrupados solo para que no incluya índices NC) para obtener el tamaño de una tabla en particular en mi base de datos. Para fines de facturación (cobramos a nuestros departamentos por la cantidad de espacio que usan) necesito calcular cuánto espacio usó cada departamento en esta tabla. Tengo una consulta que identifica cada grupo dentro de la tabla. Solo necesito calcular cuánto espacio ocupa cada grupo.
El espacio por fila puede oscilar enormemente debido a los VARCHAR(MAX)
campos en la tabla, por lo que no puedo tomar un tamaño promedio * de la proporción de filas para un departamento. Cuando uso el DATALENGTH()
enfoque descrito anteriormente, solo obtengo el 85% del espacio total utilizado en la consulta a continuación. Pensamientos?
SELECT
s.Name AS SchemaName,
t.NAME AS TableName,
p.rows AS RowCounts,
(SUM(a.total_pages) * 8)/1024 AS TotalSpaceMB,
(SUM(a.used_pages) * 8)/1024 AS UsedSpaceMB,
((SUM(a.total_pages) - SUM(a.used_pages)) * 8)/1024 AS UnusedSpaceMB
FROM
sys.tables t with (nolock)
INNER JOIN
sys.schemas s with (nolock) ON s.schema_id = t.schema_id
INNER JOIN
sys.indexes i with (nolock) ON t.OBJECT_ID = i.object_id
INNER JOIN
sys.partitions p with (nolock) ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
INNER JOIN
sys.allocation_units a with (nolock) ON p.partition_id = a.container_id
WHERE
t.is_ms_shipped = 0
AND i.OBJECT_ID > 255
AND i.type_desc = 'Clustered'
GROUP BY
t.Name, s.Name, p.Rows
ORDER BY
TotalSpaceMB desc
Se ha sugerido que cree un índice filtrado para cada departamento o partición de la tabla, de modo que pueda consultar directamente el espacio utilizado por índice. Los índices filtrados se pueden crear mediante programación (y se vuelven a colocar durante una ventana de mantenimiento o cuando necesito realizar la facturación periódica), en lugar de usar el espacio todo el tiempo (las particiones serían mejores a este respecto).
Me gusta esa sugerencia y normalmente haría eso. Pero para ser honesto, uso "cada departamento" como ejemplo para explicar por qué necesito esto, pero para ser honesto, ese no es realmente el motivo. Debido a razones de confidencialidad, no puedo explicar la razón exacta por la que necesito estos datos, pero es análogo a los diferentes departamentos.
Con respecto a los índices no agrupados en esta tabla: si puedo obtener los tamaños de los índices NC, sería genial. Sin embargo, los índices NC representan <1% del tamaño del índice agrupado, por lo que estamos bien sin incluirlos. Sin embargo, ¿cómo incluiríamos los índices NC de todos modos? Ni siquiera puedo obtener un tamaño exacto para el índice agrupado :)
fuente
Respuestas:
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):
DBCC PAGE
, por lo que se mantiene separado aquí en lugar de incluirse en la información por fila a continuación.NULL
. 1 byte por cada conjunto de 8 columnas. Y para todas las columnas, inclusoNOT NULL
las. Por lo tanto, mínimo 1 byte.ALLOW_SNAPSHOT_ISOLATION ON
oREAD_COMMITTED_SNAPSHOT ON
).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:TEXT
,NTEXT
yIMAGE
):text in row
opción, entonces:VARCHAR(MAX)
,NVARCHAR(MAX)
yVARBINARY(MAX)
):large value types out of row
opción, siempre use un puntero de 16 bytes para almacenamiento LOB.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;
BusqueParentObject
= "ENCABEZADO DE PÁGINA:" yField
= "m_freeCnt". ElValue
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 laFILLFACTOR
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. LaSPARSE
opción hace que elNULL
tipo de valor aumente 0 bytes (en lugar de la cantidad normal de longitud fija, como 4 bytes para unINT
), 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 queDATALENGTH
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 haySPARSE
columnas a través de:Y luego, para cada
SPARSE
columna, actualice la consulta original para usar: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 ServerDBCC IND(0, N'dbo.table_name', 0);
):Ni el
DBCC IND
nisys.dm_db_database_page_allocations
(con esa cláusula WHERE) informará ninguna página de índice, y solo elDBCC IND
informará al menos una página IAM.DATA_COMPRESSION: si tiene habilitado
ROW
oPAGE
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 unaBIGINT
columna que de lo contrario (suponiendoSPARSE
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 sonNULL
o0
no ocupan espacio y simplemente se indican como estarNULL
o "vacíos" (es decir0
) en una matriz que asigna las columnas. Y hay muchas, muchas otras reglas. Los datos han Unicode (NCHAR
,NVARCHAR(1 - 4000)
pero noNVARCHAR(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 cualquierFILLFACTOR
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 de8
para multiplicarnumber of pages
es demasiado alto. Qué tal si:Por lo tanto, use algo como:
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 elDATALENGTH
por cada campo, filtrando en cada "departamento":ALLOW_SNAPSHOT_ISOLATION
o estáREAD_COMMITTED_SNAPSHOT
establecida enON
)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 cadaMAX
campo.Por lo tanto, use algo como:
En general (encabezado de fila + número de columnas + matriz de ranuras + mapa de bits NULL):
En general (detección automática si hay "información de versión"):
SI hay columnas de longitud variable, agregue:
SI hay
MAX
columnas / LOB, luego agregue:En general:
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 informabasys.dm_db_database_page_allocations
y no eran los mismos valores para el número de páginas. En una corazonada, eliminé las funciones agregadasGROUP BY
y reemplacé laSELECT
lista cona.*, '---' 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 entresys.allocation_units
ysys.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 menudopartition_id
yhobt_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 aSCHEMA_NAME()
) ysys.indexes
(el índice agrupado es siempreindex_id = 1
y tenemosindex_id
ensys.partitions
).fuente
Tal vez esta sea una respuesta grunge, pero esto es lo que haría.
Entonces DATALENGTH solo representa el 86% del total. Todavía es una división muy representativa. La sobrecarga en la excelente respuesta de srutzky debería tener una división bastante pareja.
Usaría su segunda consulta (páginas) para el total. Y use el primero (longitud de datos) para asignar la división. Muchos costos se asignan utilizando una normalización.
Y debe tener en cuenta que una respuesta más cercana aumentará los costos, por lo que incluso el departamento que perdió en una división aún puede pagar más.
fuente