512 bytes não estão sendo usados ​​na página de dados de 8 KByte do SQL Server

13

Eu criei a seguinte tabela:

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

e, em seguida, criou um índice em cluster:

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

Em seguida, preenchi-o com 30 linhas, cada tamanho com 256 bytes (com base na declaração da tabela):

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;

Agora, com base nas informações que li no "Kit de treinamento (Exame 70-461): Consultando o Microsoft SQL Server 2012 (Itzik Ben-Gan)" no livro:

O SQL Server organiza internamente os dados em um arquivo de dados em páginas. Uma página é uma unidade de 8 KB e pertence a um único objeto; por exemplo, para uma tabela ou um índice. Uma página é a menor unidade de leitura e escrita. As páginas são organizadas em extensão. Uma extensão consiste em oito páginas consecutivas. As páginas de uma extensão podem pertencer a um único objeto ou a vários objetos. Se as páginas pertencerem a vários objetos, a extensão será chamada de extensão mista; se as páginas pertencerem a um único objeto, a extensão será chamada de extensão uniforme. O SQL Server armazena as oito primeiras páginas de um objeto em extensões mistas. Quando um objeto excede oito páginas, o SQL Server aloca extensões uniformes adicionais para esse objeto. Com essa organização, objetos pequenos desperdiçam menos espaço e objetos grandes são menos fragmentados.

Portanto, aqui tenho a primeira página de 8 KB de extensão mista, preenchida com 7680 bytes (inseri uma linha de tamanho de 30 vezes 256 bytes, portanto, 30 * 256 = 7680), para verificar o tamanho em que corri proc de verificação de tamanho - ele retorna o seguinte 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

Portanto, 16 KB são reservados para a tabela, a primeira página de 8 KB é para a página Root IAM, a segunda é para a página de armazenamento de dados em folha, com 8 KB com ocupação de ~ 7,5 KB, agora quando insiro uma nova linha com 256 bytes:

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

ele não é armazenado na mesma página, embora tenha um espaço de 256 bytes (7680 b + 256 = 7936, que ainda é menor que 8 KB), uma nova página de dados é criada, mas essa nova linha pode caber na mesma página antiga , por que o SQL Server cria uma nova página quando pode economizar espaço e tempo de pesquisa, inserindo-a na página existente?

Nota: o mesmo está acontecendo no índice de heap.

Alphas Supremum
fonte

Respostas:

9

Suas linhas de dados não têm 256 bytes. Cada um é mais parecido com 263 bytes. Uma linha de dados de tipos de dados com comprimento puramente fixo tem sobrecarga adicional devido à estrutura de uma linha de dados no SQL Server. Dê uma olhada neste site e leia sobre como uma linha de dados é composta. http://aboutsqlserver.com/2013/10/15/sql-server-storage-engine-data-pages-and-data-rows/

Portanto, no seu exemplo, você tem uma linha de dados com 256 bytes, adicione 2 bytes para bits de status, 2 bytes para número de colunas, 2 bytes para comprimento de dados e outros 1 ou mais para bitmap nulo. Isso é 263 * 30 = 7.890 bytes. Adicione outro 263 e você estará acima do limite de 8 KB que forçaria a criação de outra página.

dfundako
fonte
O link que você forneceu me ajudou a melhor visual da estrutura da página, eu estava procurando algo semelhante a ele, mas não conseguiu encontrá-lo, Thax
Alphas Supremum
11

Embora seja verdade que o SQL Server usa páginas de dados de 8k (8192 bytes) para armazenar 1 ou mais linhas, cada página de dados possui alguma sobrecarga (96 bytes) e cada linha tem alguma sobrecarga (pelo menos 9 bytes). Os 8192 bytes não são puramente dados.

Para um exame mais detalhado de como isso funciona, consulte minha resposta para a seguinte pergunta do DBA.SE:

SOMA de DATALENGTHs que não corresponde ao tamanho da tabela de sys.allocation_units

Usando as informações nessa resposta vinculada, podemos obter uma imagem mais clara do tamanho real da linha:

  1. Cabeçalho da linha = 4 bytes
  2. Número de colunas = 2 bytes
  3. Bitmap NULL = 1 byte
  4. Informações da versão ** = 14 bytes (opcional, consulte a nota de rodapé)
  5. Sobrecarga total por linha (excluindo matriz de slot) = no mínimo 7 bytes ou 21 bytes se houver informações sobre a versão
  6. Tamanho real total da linha = 263 mínimo (256 dados + 7 indiretos) ou 277 bytes (256 dados + 21 indiretos) se houver informações sobre a versão
  7. Adicionando na Matriz de Slot, o espaço total ocupado por linha é na verdade 265 bytes (sem informações da versão) ou 279 bytes (com informações da versão).

O uso DBCC PAGEconfirma meu cálculo mostrando: Record Size 263(para tempdb) e Record Size 277(para um banco de dados definido como ALLOW_SNAPSHOT_ISOLATION ON).

Agora, com 30 linhas, ou seja:

  • SEM informações da versão

    30 * 263 nos daria 7890 bytes. Em seguida, adicione os 96 bytes do cabeçalho da página aos 7986 bytes usados. Por fim, adicione os 60 bytes (2 por linha) da matriz de slots para um total de 8046 bytes usados ​​na página e 146 restantes. O uso DBCC PAGEconfirma meu cálculo mostrando:

    • m_slotCnt 30 (ou seja, número de linhas)
    • m_freeCnt 146 (ou seja, número de bytes restantes na página)
    • m_freeData 7986 (isto é, dados + cabeçalho da página - 7890 + 96 - a matriz de slots não é fatorada no cálculo de bytes "usados")
  • COM informações da versão

    30 * 277 bytes para um total de 8310 bytes. Mas o 8310 está acima de 8192, e isso nem foi responsável pelo cabeçalho da página de 96 bytes nem pela matriz de slots de 2 bytes por linha (30 * 2 = 60 bytes), o que deve fornecer apenas 8036 bytes utilizáveis ​​para as linhas.

    MAS, que tal 29 linhas? Isso nos daria 8033 bytes de dados (29 * 277) + 96 bytes para o cabeçalho da página + 58 bytes para a matriz de slots (29 * 2) igual a 8187 bytes. E isso deixaria a página com 5 bytes restantes (8192 - 8187; inutilizável, é claro). O uso DBCC PAGEconfirma meu cálculo mostrando:

    • m_slotCnt 29 (ou seja, número de linhas)
    • m_freeCnt 5 (ou seja, número de bytes restantes na página)
    • m_freeData 8129 (isto é, dados + cabeçalho da página - 8033 + 96 - a matriz de slots não é fatorada no cálculo de bytes "usados")

Em relação aos montões

As pilhas preenchem as páginas de dados de maneira um pouco diferente. Eles mantêm uma estimativa muito aproximada da quantidade de espaço restante na página. Ao olhar para a saída DBCC, olhar para a linha para: PAGE HEADER: Allocation Status PFS (1:1). Você verá VALUEalgo mostrando ao longo das linhas de 0x60 MIXED_EXT ALLOCATED 0_PCT_FULL(quando eu olhei para a tabela em cluster) ou0x64 MIXED_EXT ALLOCATED 100_PCT_FULL quando Heap. Isso é avaliado por transação, portanto, fazer inserções individuais, como o teste que está sendo realizado aqui, pode mostrar resultados diferentes entre as tabelas Cluster e Heap. Executar uma única operação DML para todas as 30 linhas, no entanto, preencherá a pilha conforme o esperado.

No entanto, nenhum desses detalhes sobre os Heaps afeta diretamente esse teste específico, pois as duas versões da tabela cabem em 30 linhas, com apenas 146 bytes restantes. Isso não é espaço suficiente para outra linha, independentemente de Clustered ou Heap.

Lembre-se de que este teste é bastante simples. O cálculo do tamanho real de uma linha pode ficar muito complicado dependendo de vários fatores, como: SPARSECompactação de dados, dados LOB, etc.


Para ver os detalhes da página de dados, use a seguinte 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;

** O valor "versão info" de 14 bytes estará presente se o seu banco de dados estiver definido como ALLOW_SNAPSHOT_ISOLATION ONou READ_COMMITTED_SNAPSHOT ON.

Solomon Rutzky
fonte
8060 bytes por página estão disponíveis para dados do usuário, para ser mais preciso. Os dados do OP ainda estão abaixo disso.
21416 Roger Wolf
As informações da versão não estão disponíveis, caso contrário, 30 linhas levariam 8310 bytes. O resto parece estar correto.
Roger Lobo
@ RogerWolf Sim, "informações da versão" está lá. E sim, 30 linhas requerem 8310 bytes. É por isso que essas 30 linhas, de fato, não se encaixam em uma página, pois o OP está sendo levado a acreditar em qualquer processo de teste que o OP esteja usando. Mas esse processo de teste está errado. Apenas 29 linhas cabem na página. Eu confirmei isso (mesmo usando o SQL Server 2012).
Solomon Rutzky
você tentou executar seu teste em um banco de dados que não é compatível com RCSI / tempdb? Consegui reproduzir os números exatos fornecidos pelo OP.
Roger Lobo
@RogerWolf O banco de dados que estou usando não está habilitado para RCSI, mas está definido como ALLOW_SNAPSHOT_ISOLATION. Eu também tentei tempdbe vi que as "informações da versão" não estão lá, portanto, 30 linhas se encaixam. Vou atualizar para adicionar as novas informações.
Solomon Rutzky
3

A estrutura real da página de dados é bastante complexa. Embora seja geralmente declarado que 8060 bytes por página estão disponíveis para dados do usuário, há alguma sobrecarga adicional não contada em nenhum lugar que resulte nesse comportamento.

No entanto, você deve ter notado que o SQL Server na verdade dá uma dica de que a 31ª linha não cabe na página. Para que a próxima linha caiba na mesma página, o avg_page_space_used_in_percentvalor deve estar abaixo de 100% - (100/31) = 96.774194 e, no seu caso, está muito acima disso.

PS: Creio ter visto uma explicação detalhada e detalhada da estrutura da página de dados em um dos livros "SQL Server Internals" de Kalen Delaney, mas foi há quase 10 anos, então, desculpe-me por não me lembrar de mais detalhes. Além disso, a estrutura da página tende a mudar de versão para versão.

Roger Wolf
fonte
1
Não. O uniquificador é adicionado apenas às linhas-chave duplicadas. A primeira linha de cada valor de chave exclusivo não inclui o uniquificador extra de 4 bytes.
Solomon Rutzky 14/08
@srutzky, aparentemente você está certo. Nunca pensei que o SQL Server permitiria chaves de largura variável. Isso é feio. Eficiente, sim, mas feio.
Roger Lobo