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.
Dados não são a única coisa que ocupa espaço em uma página de dados de 8k:
Há espaço reservado. Você só pode usar 8060 dos 8192 bytes (ou seja, 132 bytes que nunca foram seus):
- Cabeçalho da página: são exatamente 96 bytes.
- Matriz de slot: é de 2 bytes por linha e indica o deslocamento de onde cada linha começa na página. O tamanho dessa matriz não está limitado aos 36 bytes restantes (132 - 96 = 36); caso contrário, você estaria efetivamente limitado a colocar apenas 18 linhas no máximo em uma página de dados. Isso significa que cada linha é 2 bytes maior do que você pensa. Esse valor não é incluído no "tamanho do registro", conforme relatado por
DBCC PAGE
, e é por isso que é mantido aqui separado, em vez de ser incluído nas informações por linha abaixo.
- Meta-dados por linha (incluindo, entre outros):
- O tamanho varia dependendo da definição da tabela (ou seja, número de colunas, comprimento variável ou comprimento fixo, etc.). Informações retiradas dos comentários de @ PaulWhite e @ Aaron que podem ser encontradas na discussão relacionada a esta resposta e teste.
- Cabeçalho da linha: 4 bytes, 2 deles indicando o tipo de registro e os outros dois sendo um deslocamento para o bitmap NULL
- Número de colunas: 2 bytes
- Bitmap NULL: quais colunas estão atualmente
NULL
. 1 byte por cada conjunto de 8 colunas. E para todas as colunas, mesmo NOT NULL
as. Portanto, no mínimo 1 byte.
- Matriz de deslocamento da coluna de comprimento variável: mínimo de 4 bytes. 2 bytes para armazenar o número de colunas de comprimento variável e, em seguida, 2 bytes por cada coluna de comprimento variável para manter o deslocamento no local onde ele inicia.
- Informações sobre versão: 14 bytes (isso estará presente se seu banco de dados estiver definido como
ALLOW_SNAPSHOT_ISOLATION ON
ou READ_COMMITTED_SNAPSHOT ON
).
- Consulte a seguinte pergunta e resposta para obter mais detalhes sobre isso: Matriz de slot e tamanho total da página
- Consulte a seguinte publicação de blog de Paul Randall, que possui vários detalhes interessantes sobre como as páginas de dados são organizadas: Analisando com DBCC PAGE (Parte 1 de?)
Ponteiros LOB para dados que não são armazenados em linha. Portanto, isso representaria DATALENGTH
+ pointer_size. Mas estes não são de tamanho padrão. Consulte a seguinte publicação no blog para obter detalhes sobre este tópico complexo: Qual é o tamanho do ponteiro LOB para tipos (MAX) como Varchar, Varbinary, Etc? . Entre essa postagem vinculada e alguns testes adicionais que eu fiz , as regras (padrão) devem ser as seguintes:
- Legado / obsoleto tipos LOB que ninguém deve estar usando mais a partir de SQL Server 2005 (
TEXT
, NTEXT
e IMAGE
):
- Por padrão, sempre armazene seus dados em páginas LOB e sempre use um ponteiro de 16 bytes para armazenamento LOB.
- Se sp_tableoption foi usado para definir a
text in row
opção, então:
- se houver espaço na página para armazenar o valor, e o valor não for maior que o tamanho máximo em linha (intervalo configurável de 24 a 7000 bytes com um padrão de 256), ele será armazenado em linha,
- caso contrário, será um ponteiro de 16 bytes.
- Para os tipos LOB mais recentes introduzidas no SQL Server 2005 (
VARCHAR(MAX)
, NVARCHAR(MAX)
e VARBINARY(MAX)
):
- Por padrão:
- Se o valor não for maior que 8000 bytes e houver espaço na página, ele será armazenado em linha.
- Raiz Inline - para dados entre 8001 e 40.000 (realmente 42.000) bytes, se o espaço permitir, haverá 1 a 5 ponteiros (24 - 72 bytes) IN ROW que apontam diretamente para a (s) página (s) LOB. 24 bytes para a página inicial de 8k LOB e 12 bytes por cada página adicional de 8k para até mais quatro páginas de 8k.
- TEXT_TREE - para dados com mais de 42.000 bytes, ou se os ponteiros de 1 a 5 não puderem caber em linha, haverá apenas um ponteiro de 24 bytes para a página inicial de uma lista de ponteiros para as páginas LOB (ou seja, a "árvore de texto" " página).
- Se sp_tableoption foi usado para definir a
large value types out of row
opção, use sempre um ponteiro de 16 bytes para armazenamento LOB.
- Eu disse regras "padrão" porque não testei valores em linha contra o impacto de certos recursos, como compactação de dados, criptografia no nível da coluna, criptografia transparente de dados, sempre criptografado etc.
Páginas de estouro de LOB: se um valor for 10k, isso exigirá 1 página de 8k de estouro e parte da segunda página. Se nenhum outro dado puder ocupar o espaço restante (ou é permitido, não tenho certeza dessa regra), você terá aproximadamente 6kb de espaço "desperdiçado" nessa segunda página de dados de estouro de LOB.
Espaço não utilizado: uma página de dados de 8k é exatamente isso: 8192 bytes. Não varia em tamanho. Os dados e metadados colocados nele, no entanto, nem sempre se encaixam perfeitamente em todos os 8192 bytes. E as linhas não podem ser divididas em várias páginas de dados. Portanto, se você tiver 100 bytes restantes, mas nenhuma linha (ou nenhuma linha que se encaixaria nesse local, dependendo de vários fatores) pode caber lá, a página de dados continuará ocupando 8192 bytes e sua 2ª consulta contará apenas o número de páginas de dados. Você pode encontrar esse valor em dois lugares (lembre-se de que parte desse valor é uma parte desse espaço reservado):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
Procure ParentObject
= "PAGE HEADER:" e Field
= "m_freeCnt". O Value
campo é o número de bytes não 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 é o mesmo valor relatado por "m_freeCnt". Isso é mais fácil que o DBCC, pois pode obter muitas páginas, mas também exige que as páginas tenham sido lidas no buffer pool em primeiro lugar.
Espaço reservado por FILLFACTOR
<100. As páginas criadas recentemente não respeitam a FILLFACTOR
configuração, mas executar um REBUILD reservará esse espaço em cada página de dados. A idéia por trás do espaço reservado é que ele será usado por inserções não sequenciais e / ou atualizações que já expandem o tamanho das linhas da página, devido à atualização de colunas de comprimento variável com um pouco mais de dados (mas não o suficiente para causar uma divisão de página). Mas você pode facilmente reservar espaço em páginas de dados que naturalmente nunca receberão novas linhas e nunca terão as linhas existentes atualizadas ou, pelo menos, não atualizadas de maneira a aumentar o tamanho da linha.
Divisões de página (fragmentação): a necessidade de adicionar uma linha a um local que não tenha espaço para a linha causará uma divisão de página. Nesse caso, aproximadamente 50% dos dados existentes são movidos para uma nova página e a nova linha é adicionada a uma das 2 páginas. Mas agora você tem um pouco mais de espaço livre que não é contabilizado pelos DATALENGTH
cálculos.
Linhas marcadas para exclusão. Quando você exclui linhas, elas nem sempre são removidas imediatamente da página de dados. Se não puderem ser removidos imediatamente, serão "marcados para morte" (referência de Steven Segal) e serão fisicamente removidos posteriormente pelo processo de limpeza de fantasmas (acredito que esse seja o nome). No entanto, estes podem não ser relevantes para esta questão em particular.
Páginas fantasmas? Não tenho certeza se esse é o termo adequado, mas às vezes as páginas de dados não são removidas até que uma REBUILD do Clustered Index seja concluída. Isso também seria responsável por mais páginas do DATALENGTH
que as somadas. Isso geralmente não deveria acontecer, mas já o encontrei uma vez, há vários anos.
Colunas esparsas: as colunas esparsas economizam espaço (principalmente para tipos de dados de comprimento fixo) em tabelas em que uma grande% das linhas é NULL
para uma ou mais colunas. A SPARSE
opção NULL
aumenta o valor do tipo 0 bytes (em vez da quantidade normal de comprimento fixo, como 4 bytes para um INT
), mas os valores diferentes de NULL ocupam 4 bytes adicionais para os tipos de comprimento fixo e uma quantidade variável para tipos de comprimento variável. O problema aqui é que DATALENGTH
não inclui os 4 bytes extras para valores diferentes de NULL em uma coluna SPARSE; portanto, esses 4 bytes precisam ser adicionados novamente. Você pode verificar se há SPARSE
colunas atravé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;
E, em seguida, para cada SPARSE
coluna, atualize a consulta original para usar:
SUM(DATALENGTH(FieldN) + 4)
Observe que o cálculo acima para adicionar um padrão de 4 bytes é um pouco simplista, pois funciona apenas para tipos de comprimento fixo. E, há metadados adicionais por linha (pelo que posso dizer até agora) que reduzem o espaço disponível para os dados, simplesmente tendo pelo menos uma coluna SPARSE. Para mais detalhes, consulte a página do MSDN para Usar colunas esparsas .
Índice e outras páginas (por exemplo, IAM, PFS, GAM, SGAM, etc): essas não são páginas de "dados" em termos de dados do usuário. Isso aumentará o tamanho total da tabela. Se você estiver usando o SQL Server 2012 ou mais recente, poderá usar a sys.dm_db_database_page_allocations
DMF (Dynamic Management Function) para ver os tipos de página (as versões anteriores do SQL Server podem usar 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
Nem o DBCC IND
nor sys.dm_db_database_page_allocations
(com essa cláusula WHERE) reportará nenhuma página de índice e apenas o DBCC IND
reportará pelo menos uma página do IAM.
DATA_COMPRESSION: se você tiver ROW
ou a PAGE
Compactação ativada no Clustered Index ou Heap, poderá esquecer a maior parte do que foi mencionado até agora. O cabeçalho da página de 96 bytes, a matriz de slot de 2 bytes por linha e as informações de versão de 14 bytes por linha ainda estão lá, mas a representação física dos dados se torna altamente complexa (muito mais do que o que já foi mencionado quando o Compactação não está sendo usado). Por exemplo, com compactação de linha, o SQL Server tenta usar o menor contêiner possível para caber em cada coluna, por cada linha. Portanto, se você tiver uma BIGINT
coluna que, caso contrário (supondo que SPARSE
também não esteja ativada), sempre ocupará 8 bytes, se o valor estiver entre -128 e 127 (ou seja, número inteiro de 8 bits assinado), ele usará apenas 1 byte e se o valor valor poderia caber em umSMALLINT
, ocupará apenas 2 bytes. Os tipos inteiros que ou são NULL
ou 0
não ocupam espaço e são simplesmente indicados como sendo NULL
ou "vazias" (isto é, 0
) em uma matriz de mapeamento para as colunas. E há muitas, muitas outras regras. Dados têm Unicode ( NCHAR
, NVARCHAR(1 - 4000)
mas não NVARCHAR(MAX)
, mesmo se armazenado em-linha)? A compactação Unicode foi adicionada no SQL Server 2008 R2, mas não há como prever o resultado do valor "compactado" em todas as situações sem fazer a compactação real, dada a complexidade das regras .
Realmente, sua segunda consulta, embora mais precisa em termos de espaço físico total ocupado em disco, só é realmente precisa ao executar um REBUILD
dos índices de cluster. E depois disso, você ainda precisa contabilizar qualquer FILLFACTOR
configuração abaixo de 100. E mesmo assim, sempre há cabeçalhos de página e, com frequência, uma quantidade suficiente de espaço "desperdiçado" que simplesmente não é preenchível por ser muito pequena para caber em qualquer linha dessa linha. tabela, ou pelo menos a linha que logicamente deve ir nesse slot.
Com relação à precisão da 2ª consulta na determinação do "uso de dados", parece mais justo recuperar os bytes do cabeçalho da página, pois eles não são uso de dados: são custos indiretos de custo dos negócios. Se houver 1 linha em uma página de dados e essa linha for apenas a TINYINT
, esse byte ainda exigirá que a página de dados existisse e, portanto, os 96 bytes do cabeçalho. Esse departamento deve ser cobrado por toda a página de dados? Se essa página de dados for preenchida pelo Departamento 2, eles dividirão uniformemente esse custo "adicional" ou pagarão proporcionalmente? Parece mais fácil apenas fazer o backup. Nesse caso, usar um valor de 8
para multiplicar number of pages
é muito alto. E se:
-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250
Portanto, use algo como:
(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
para todos os cálculos nas colunas "número_de_páginas".
E , considerando que o uso DATALENGTH
por cada campo não pode retornar os metadados por linha, que devem ser adicionados à sua consulta por tabela onde você obtém o DATALENGTH
por cada campo, filtrando cada "departamento":
- Tipo de registro e deslocamento para Bitmap NULL: 4 bytes
- Contagem de colunas: 2 bytes
- Matriz de slot: 2 bytes (não incluído no "tamanho do registro", mas ainda precisa ser considerado)
- Bitmap NULL: 1 byte a cada 8 colunas (para todas as colunas)
- Versão de linha: 14 bytes (se ambos os banco de dados tem
ALLOW_SNAPSHOT_ISOLATION
ou READ_COMMITTED_SNAPSHOT
definir a ON
)
- Matriz de deslocamento da coluna de comprimento variável: 0 bytes se todas as colunas forem de comprimento fixo. Se alguma coluna tiver comprimento variável, 2 bytes, mais 2 bytes por cada uma das colunas de comprimento variável.
- Ponteiros LOB: esta parte é muito imprecisa, pois não haverá um ponteiro se o valor for
NULL
e, se o valor se ajustar à linha, ele poderá ser muito menor ou muito maior que o ponteiro e se o valor for armazenado. linha, o tamanho do ponteiro pode depender da quantidade de dados que há. No entanto, como queremos apenas uma estimativa (ou seja, "swag"), parece que 24 bytes é um bom valor para usar (bem, tão bom quanto qualquer outro ;-). Este é o MAX
campo por cada .
Portanto, use algo como:
Em geral (cabeçalho da linha + número de colunas + matriz do slot + bitmap NULL):
([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
Em geral (detecção automática se "informações da versão" estiverem presentes):
+ (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 houver colunas de tamanho variável, adicione:
+ 2 + (2 * {NumVariableLengthColumns})
Se houver alguma MAX
coluna / LOB, adicione:
+ (24 * {NumLobColumns})
Em geral:
)) AS [MetaDataBytes]
Isso não é exato e, novamente, não funcionará se você tiver a compactação de linha ou de página ativada no heap ou no índice clusterizado, mas definitivamente deve aproximá-lo.
ATUALIZAÇÃO sobre o mistério da diferença de 15%
Nós (inclusive eu) estávamos tão focados em pensar em como as páginas de dados são dispostas e em como isso DATALENGTH
pode explicar coisas que não passamos muito tempo revisando a segunda consulta. Executei essa consulta em uma única tabela e comparei esses valores com o que estava sendo relatado sys.dm_db_database_page_allocations
e eles não eram os mesmos valores para o número de páginas. Em um palpite, removi as funções agregadas GROUP BY
e substituí a SELECT
lista por a.*, '---' AS [---], p.*
. E então ficou claro: as pessoas devem ter cuidado de onde nessas interwebs obscuras elas obtêm suas informações e scripts de ;-). A segunda consulta postada na pergunta não está exatamente correta, especialmente para essa pergunta em particular.
Problema menor: fora dele não faz muito sentido GROUP BY rows
(e não tem essa coluna em uma função agregada), a junção entre sys.allocation_units
e sys.partitions
não é tecnicamente correta. Existem 3 tipos de unidades de alocação, e uma delas deve se unir a um campo diferente. Muitas vezes partition_id
e hobt_id
são os mesmos, portanto, talvez nunca haja um problema, mas às vezes esses dois campos têm valores diferentes.
Grande problema: a consulta usa o used_pages
campo Esse campo abrange todos os tipos de páginas: Dados, Índice, IAM, etc, tc. Há um outro campo, mais apropriado para uso quando em causa apenas com os dados reais: data_pages
.
Adaptei a 2ª consulta da pergunta com os itens acima em mente e usando o tamanho da página de dados que faz o retorno do cabeçalho da página. Também removi dois JOINs desnecessários: sys.schemas
(substituído por call to SCHEMA_NAME()
) e sys.indexes
(o Clustered Index é sempre index_id = 1
e temos index_id
dentro 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;