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 NULLas. 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 ONou 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, NTEXTe 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 rowopçã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 rowopçã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 Valuecampo é 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 FILLFACTORconfiguraçã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 DATALENGTHcá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 DATALENGTHque 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 é NULLpara uma ou mais colunas. A SPARSEopção NULLaumenta 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 DATALENGTHnã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á SPARSEcolunas 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 SPARSEcoluna, 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_allocationsDMF (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 INDnor sys.dm_db_database_page_allocations(com essa cláusula WHERE) reportará nenhuma página de índice e apenas o DBCC INDreportará pelo menos uma página do IAM.
DATA_COMPRESSION: se você tiver ROWou a PAGECompactaçã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 BIGINTcoluna que, caso contrário (supondo que SPARSEtambé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 NULLou 0não ocupam espaço e são simplesmente indicados como sendo NULLou "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 REBUILDdos índices de cluster. E depois disso, você ainda precisa contabilizar qualquer FILLFACTORconfiguraçã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 8para 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 DATALENGTHpor cada campo não pode retornar os metadados por linha, que devem ser adicionados à sua consulta por tabela onde você obtém o DATALENGTHpor 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_ISOLATIONou READ_COMMITTED_SNAPSHOTdefinir 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
NULLe, 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 MAXcampo 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 MAXcoluna / 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 DATALENGTHpode 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_allocationse eles não eram os mesmos valores para o número de páginas. Em um palpite, removi as funções agregadas GROUP BYe substituí a SELECTlista 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_unitse sys.partitionsnão é tecnicamente correta. Existem 3 tipos de unidades de alocação, e uma delas deve se unir a um campo diferente. Muitas vezes partition_ide hobt_idsã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_pagescampo 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 = 1e temos index_iddentro 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;