512 bytes no se utilizan desde la página de datos de 8 KByte de SQL Server

13

He creado la siguiente tabla:

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

y luego creó un índice agrupado:

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

Luego lo rellené con 30 filas cada tamaño es de 256 bytes (según la declaración de la tabla):

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;

Ahora, según la información que leí en el libro "Kit de capacitación (examen 70-461): Consulta de Microsoft SQL Server 2012 (Itzik Ben-Gan)":

SQL Server organiza internamente los datos en un archivo de datos en páginas. Una página es una unidad de 8 KB y pertenece a un solo objeto; por ejemplo, a una tabla o un índice. Una página es la unidad más pequeña de lectura y escritura. Las páginas se organizan en extensiones. Una extensión consta de ocho páginas consecutivas. Las páginas de una extensión pueden pertenecer a un solo objeto o a varios objetos. Si las páginas pertenecen a varios objetos, la extensión se denomina extensión mixta; Si las páginas pertenecen a un solo objeto, la extensión se denomina extensión uniforme. SQL Server almacena las primeras ocho páginas de un objeto en extensiones mixtas. Cuando un objeto excede las ocho páginas, SQL Server asigna extensiones uniformes adicionales para este objeto. Con esta organización, los objetos pequeños desperdician menos espacio y los objetos grandes están menos fragmentados.

Así que aquí tengo la primera página de 8 KB de extensión mixta, poblada con 7680 bytes (he insertado 30 veces la fila de 256 bytes de tamaño, por lo que 30 * 256 = 7680), para verificar el tamaño que he ejecutado, compruebe el tamaño del proceso: devuelve el siguiente resultado

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

Entonces, 16 KB están reservados para la tabla, la primera página de 8 KB es para la página Root IAM, la segunda es para la página de almacenamiento de datos de hoja que es de 8 KB con una ocupación de ~ 7.5 KB, ahora cuando inserto una nueva fila con 256 Byte:

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

no se almacena en la misma página, aunque tiene un espacio de 256 bytes (7680 b + 256 = 7936, que aún es más pequeño que 8 KB), se crea una nueva página de datos, pero esa nueva fila podría caber en la misma página anterior , ¿por qué SQL Server crea una nueva página cuando podría ahorrar espacio y tiempo de búsqueda al insertarla en la página existente?

Nota: lo mismo está sucediendo en el índice de montón.

Supremum Alphas
fuente

Respuestas:

9

Sus filas de datos no son 256 bytes. Cada uno es más como 263 bytes. Una fila de datos de tipos de datos de longitud puramente fija tiene una sobrecarga adicional debido a la estructura de una fila de datos en SQL Server. Eche un vistazo a este sitio y lea acerca de cómo se compone una fila de datos. http://aboutsqlserver.com/2013/10/15/sql-server-storage-engine-data-pages-and-data-rows/

Entonces, en su ejemplo, tiene una fila de datos que tiene 256 bytes, agregue 2 bytes para los bits de estado, 2 bytes para el número de columnas, 2 bytes para la longitud de los datos y otro 1 o más para el mapa de bits nulo. Eso es 263 * 30 = 7,890bytes. Agregue otro 263 y estará por encima del límite de página de 8 kb, lo que obligaría a crear otra página.

dfundako
fuente
El enlace que me proporcionó me ayudó a visualizar mejor la estructura de la página, estaba buscando algo similar pero no pude encontrarlo, Thax
Alphas Supremum
11

Si bien es cierto que SQL Server usa páginas de datos de 8k (8192 bytes) para almacenar 1 o más filas, cada página de datos tiene algo de sobrecarga (96 bytes) y cada fila tiene algo de sobrecarga (al menos 9 bytes). Los 8192 bytes no son puramente datos.

Para un examen más detallado de cómo funciona esto, vea mi respuesta a la siguiente pregunta de DBA.SE:

SUMA de DATALENGTHs que no coinciden con el tamaño de la tabla de sys.allocation_units

Usando la información en esa respuesta vinculada, podemos obtener una imagen más clara del tamaño real de la fila:

  1. Encabezado de fila = 4 bytes
  2. Número de columnas = 2 bytes
  3. Mapa de bits NULL = 1 byte
  4. Información de la versión ** = 14 bytes (opcional, ver nota al pie)
  5. Sobrecarga total por fila (excluyendo matriz de ranuras) = ​​7 bytes mínimo, o 21 bytes si hay información de versión
  6. Tamaño de fila real total = 263 mínimo (256 datos + 7 sobrecarga) o 277 bytes (256 datos + 21 sobrecarga) si hay información de versión
  7. Agregando en la matriz de ranuras, el espacio total ocupado por fila es en realidad 265 bytes (sin información de versión) o 279 bytes (con información de versión).

El uso DBCC PAGEconfirma mi cálculo al mostrar: Record Size 263(para tempdb) y Record Size 277(para una base de datos configurada en ALLOW_SNAPSHOT_ISOLATION ON).

Ahora, con 30 filas, es decir:

  • SIN información de versión

    30 * 263 nos daría 7890 bytes. Luego agregue los 96 bytes del encabezado de página para los 7986 bytes utilizados. Finalmente, agregue los 60 bytes (2 por fila) de la matriz de ranuras para un total de 8046 bytes utilizados en la página, y 146 restantes. El uso DBCC PAGEconfirma mi cálculo al mostrar:

    • m_slotCnt 30 (es decir, número de filas)
    • m_freeCnt 146 (es decir, número de bytes restantes en la página)
    • m_freeData 7986 (es decir, datos + encabezado de página - 7890 + 96 - la matriz de ranuras no se incluye en el cálculo de bytes "usados")
  • CON información de la versión

    30 * 277 bytes para un total de 8310 bytes. Pero 8310 está por encima de 8192, y eso ni siquiera tuvo en cuenta el encabezado de página de 96 bytes ni la matriz de ranuras de 2 bytes por fila (30 * 2 = 60 bytes), lo que debería darnos solo 8036 bytes utilizables para las filas.

    PERO, ¿qué hay de 29 filas? Eso nos daría 8033 bytes de datos (29 * 277) + 96 bytes para el encabezado de página + 58 bytes para la matriz de ranuras (29 * 2) que equivalen a 8187 bytes. Y eso dejaría la página con 5 bytes restantes (8192 - 8187; inutilizable, por supuesto). El uso DBCC PAGEconfirma mi cálculo al mostrar:

    • m_slotCnt 29 (es decir, número de filas)
    • m_freeCnt 5 (es decir, número de bytes restantes en la página)
    • m_freeData 8129 (es decir, datos + encabezado de página - 8033 + 96 - la matriz de ranuras no se incluye en el cálculo de bytes "usados")

En cuanto a montones

Los montones llenan las páginas de datos de forma ligeramente diferente. Mantienen una estimación muy aproximada de la cantidad de espacio que queda en la página. Al mirar el resultado de DBCC, mirar a la fila para: PAGE HEADER: Allocation Status PFS (1:1). Verá que VALUEmuestra algo en la línea de 0x60 MIXED_EXT ALLOCATED 0_PCT_FULL(cuando miré la tabla Agrupada) o0x64 MIXED_EXT ALLOCATED 100_PCT_FULL al mirar la tabla Montón. Esto se evalúa por transacción, por lo que hacer inserciones individuales como la prueba que se realiza aquí podría mostrar resultados diferentes entre las tablas en clúster y en el montón. Sin embargo, hacer una sola operación DML para las 30 filas llenará el montón como se esperaba.

Sin embargo, ninguno de estos detalles con respecto a los montones afecta directamente esta prueba en particular, ya que ambas versiones de la tabla se ajustan a 30 filas con solo 146 bytes restantes. Eso no es suficiente espacio para otra fila, independientemente de Clustered o Heap.

Tenga en cuenta que esta prueba es bastante simple. Calcular el tamaño real de una fila puede ser muy complicado dependiendo de varios factores, como: SPARSECompresión de datos, datos LOB, etc.


Para ver los detalles de la página de datos, use la siguiente consulta:

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;

** El valor de "información de versión" de 14 bytes estará presente si su base de datos está configurada en ALLOW_SNAPSHOT_ISOLATION ONo READ_COMMITTED_SNAPSHOT ON.

Solomon Rutzky
fuente
8060 bytes por página están disponibles para los datos del usuario, para ser precisos. Los datos del OP todavía están por debajo de eso.
Roger Wolf
La información de la versión no está allí; de lo contrario, 30 filas ocuparían 8310 bytes. El resto parece ser correcto.
Roger Wolf
@RogerWolf Sí, "información de la versión" está ahí. Y sí, 30 filas requieren 8310 bytes. Es por eso que esas 30 filas no encajan, de hecho, en 1 página, ya que el OP está siendo llevado a creer por cualquier proceso de prueba que esté utilizando el OP. Pero ese proceso de prueba está mal. Solo caben 29 filas en la página. He confirmado esto (incluso usando SQL Server 2012).
Solomon Rutzky
¿ha intentado ejecutar su prueba en una base de datos que no está habilitada para RCSI / tempdb? Pude reproducir los números exactos proporcionados por OP.
Roger Wolf
@RogerWolf La base de datos que estoy usando no está habilitada para RCSI, pero está configurada en ALLOW_SNAPSHOT_ISOLATION. También acabo de probar tempdby vi que la "información de la versión" no está allí, por lo tanto, 30 filas encajan. Actualizaré para agregar la nueva información.
Solomon Rutzky
3

La estructura real de la página de datos es bastante compleja. Si bien en general se dice que 8060 bytes por página están disponibles para los datos del usuario, hay una sobrecarga adicional que no se cuenta en ninguna parte, lo que resulta en este comportamiento.

Sin embargo, es posible que haya notado que SQL Server realmente le da una pista de que la fila 31 no cabe en la página. Para que la siguiente fila encaje en la misma página, el avg_page_space_used_in_percentvalor debe estar por debajo del 100% - (100/31) = 96.774194, y en su caso está muy por encima de eso.

PD: Creo que vi una explicación detallada, hasta el byte, de la estructura de la página de datos en uno de los libros "SQL Server Internals" de Kalen Delaney, pero fue hace casi 10 años, así que discúlpeme por no recordar más detalles. Además, la estructura de la página tiende a cambiar de una versión a otra.

Roger Wolf
fuente
1
No. El uniquifier solo se agrega a las filas de claves duplicadas. La primera fila de cada valor clave único no incluye el uniquificador adicional de 4 bytes.
Solomon Rutzky
@srutzky, aparentemente tienes razón. Nunca pensé que SQL Server permitiría claves de ancho variable. Esto es feo Eficiente, sí, pero feo.
Roger Wolf