Tive a impressão de que, se somasse DATALENGTH()
todos os campos de todos os registros em uma tabela, obteria o tamanho total da tabela. Estou enganado?
SELECT
SUM(DATALENGTH(Field1)) +
SUM(DATALENGTH(Field2)) +
SUM(DATALENGTH(Field3)) TotalSizeInBytes
FROM SomeTable
WHERE X, Y, and Z are true
Usei esta consulta abaixo (que obtive online para obter tamanhos de tabela, índices clusterizados apenas para não incluir índices NC) para obter o tamanho de uma tabela específica em meu banco de dados. Para fins de cobrança (cobramos nossos departamentos pela quantidade de espaço que eles usam), preciso descobrir quanto espaço cada departamento usou nesta tabela. Tenho uma consulta que identifica cada grupo dentro da tabela. Eu só preciso descobrir quanto espaço cada grupo está ocupando.
O espaço por linha pode oscilar muito devido aos VARCHAR(MAX)
campos da tabela, então não posso simplesmente pegar um tamanho médio * a proporção de linhas para um departamento. Quando uso a DATALENGTH()
abordagem descrita acima, obtenho apenas 85% do espaço total usado na consulta abaixo. Pensamentos?
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
Foi sugerido que eu crie um índice filtrado para cada departamento ou particione a tabela, para que eu possa consultar diretamente o espaço usado por índice. Índices filtrados poderiam ser criados programaticamente (e descartados novamente durante uma janela de manutenção ou quando eu precisar realizar o faturamento periódico), em vez de usar o espaço o tempo todo (partições seriam melhores nesse aspecto).
Eu gosto dessa sugestão e normalmente faria isso. Mas, para ser sincero, uso "cada departamento" como exemplo para explicar por que preciso disso, mas, para ser sincero, não é exatamente por isso. Por motivos de confidencialidade, não posso explicar o motivo exato pelo qual preciso desses dados, mas é análogo a diferentes departamentos.
Em relação aos índices não clusterizados nesta tabela: Se eu pudesse obter os tamanhos dos índices NC, seria ótimo. No entanto, os índices NC respondem por <1% do tamanho do índice clusterizado, portanto, podemos não incluí-los. No entanto, como incluiríamos os índices NC de qualquer maneira? Não consigo nem obter um tamanho preciso para o índice Clustered :)
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.
Os 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ó tem permissão para usar 8060 dos 8192 bytes (são 132 bytes que nunca foram seus):
DBCC PAGE
, e é por isso que ele é mantido separado aqui, em vez de ser incluído nas informações por linha abaixo.NULL
. 1 byte por cada conjunto de 8 colunas. E para todas as colunas, mesmoNOT NULL
aquelas. Portanto, mínimo 1 byte.ALLOW_SNAPSHOT_ISOLATION ON
ouREAD_COMMITTED_SNAPSHOT ON
).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 postagem de blog a seguir para obter detalhes sobre esse 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 fiz , as regras (padrão) devem ser as seguintes:TEXT
,NTEXT
eIMAGE
):text in row
opção, então:VARCHAR(MAX)
,NVARCHAR(MAX)
eVARBINARY(MAX)
):large value types out of row
opção, use sempre um ponteiro de 16 bytes para o armazenamento LOB.Páginas de excesso de LOB: Se um valor for 10k, isso exigirá 1 página completa de 8k de estouro e, em seguida, parte de uma segunda página. Se nenhum outro dado puder ocupar o espaço restante (ou mesmo for permitido, não tenho certeza dessa regra), você terá aproximadamente 6 kb de espaço "desperdiçado" na página de dados de estouro do 2º LOB.
Espaço não utilizado: Uma página de dados de 8k é exatamente isso: 8192 bytes. Não varia de tamanho. Os dados e metadados colocados nele, no entanto, nem sempre se encaixam bem 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 caberia naquele local, dependendo de vários fatores) puder caber lá, a página de dados ainda ocupará 8192 bytes e sua segunda consulta estará contando apenas o número de páginas de dados. Você pode encontrar esse valor em dois lugares (lembre-se de que parte desse valor é parte desse espaço reservado):
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
ProcureParentObject
= "PAGE HEADER:" eField
= "m_freeCnt". OValue
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 informado por "m_freeCnt". Isso é mais fácil do que o DBCC, pois pode obter muitas páginas, mas também requer que as páginas tenham sido lidas no buffer pool em primeiro lugar.Espaço reservado por
FILLFACTOR
< 100. As páginas recém-criadas não respeitam aFILLFACTOR
configuração, mas fazer uma RECONSTRUÇÃO reservará esse espaço em cada página de dados. A ideia por trás do espaço reservado é que ele será usado por inserções não sequenciais e/ou atualizações que expandem o tamanho das linhas já existentes na página, devido às colunas de comprimento variável serem atualizadas com um pouco mais de dados (mas não o suficiente para causar um divisão de página). Mas você poderia facilmente reservar espaço em páginas de dados que naturalmente nunca obteriam novas linhas e nunca teriam as linhas existentes atualizadas, ou pelo menos não atualizadas de forma 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 tem 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 eles não puderem ser removidos imediatamente, eles são "marcados para morrer" (referência a Steven Segal) e serão removidos fisicamente mais tarde pelo processo de limpeza fantasma (acredito que seja esse 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 apropriado, mas às vezes as páginas de dados não são removidas até que uma RECONSTRUÇÃO do Índice Clusterizado seja feita. Isso também representaria mais páginas do
DATALENGTH
que o total. Isso geralmente não deveria acontecer, mas já me deparei com isso uma vez, há vários anos.Colunas SPARSE: Colunas esparsas economizam espaço (principalmente para tipos de dados de comprimento fixo) em tabelas onde uma grande % das linhas são
NULL
para uma ou mais colunas. ASPARSE
opção torna oNULL
tipo de valor até 0 bytes (em vez da quantidade normal de comprimento fixo, como 4 bytes para umINT
), mas valores não NULL ocupam 4 bytes adicionais para tipos de comprimento fixo e uma quantidade variável para tipos de comprimento variável. O problema aqui é queDATALENGTH
não inclui os 4 bytes extras para valores não NULL em uma coluna SPARSE, então esses 4 bytes precisam ser adicionados novamente. Você pode verificar se há algumaSPARSE
coluna via:E então, para cada
SPARSE
coluna, atualize a consulta original a ser usada:Observe que o cálculo acima para adicionar 4 bytes padrão é um pouco simplista, pois funciona apenas para tipos de comprimento fixo. E, há metadados adicionais por linha (pelo que posso dizer até agora) que reduz o espaço disponível para dados, simplesmente por ter pelo menos uma coluna SPARSE. Para obter mais detalhes, consulte a página do MSDN para usar colunas esparsas .
Índice e outras páginas (por exemplo, IAM, PFS, GAM, SGAM, etc): não são páginas de "dados" em termos de dados do usuário. Isso aumentará o tamanho total da tabela. Se estiver usando o SQL Server 2012 ou mais recente, você pode usar a
sys.dm_db_database_page_allocations
função de gerenciamento dinâmico (DMF) para ver os tipos de página (as versões anteriores do SQL Server podem usarDBCC IND(0, N'dbo.table_name', 0);
):Nem o
DBCC IND
nemsys.dm_db_database_page_allocations
(com essa cláusula WHERE) relatará nenhuma página de índice e somente oDBCC IND
relatará pelo menos uma página IAM.DATA_COMPRESSION: Se você tiver
ROW
ouPAGE
a compactação habilitada no índice clusterizado ou no heap, poderá esquecer a maior parte do que foi mencionado até agora. O cabeçalho da página de 96 bytes, a matriz de slots 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 torna-se altamente complexa (muito mais do que já foi mencionado quando a compactação não está sendo usado). Por exemplo, com a compactação de linha, o SQL Server tenta usar o menor contêiner possível para caber em cada coluna, em cada linha. Portanto, se você tiver umaBIGINT
coluna que, de outra forma (supondo queSPARSE
também não esteja habilitada), sempre ocuparia 8 bytes, se o valor estiver entre -128 e 127 (ou seja, inteiro de 8 bits com sinal), usará apenas 1 byte e, se o valor pode caber em umSMALLINT
, ele ocupará apenas 2 bytes. Tipos inteiros que sãoNULL
ou0
não ocupam espaço e são simplesmente indicados como sendoNULL
ou "vazios" (isto é0
, ) em um array mapeando as colunas. E há muitas, muitas outras regras. Possui dados Unicode (NCHAR
,NVARCHAR(1 - 4000)
, mas nãoNVARCHAR(MAX)
, mesmo se armazenados 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 devido à complexidade das regras .Então, realmente, sua segunda consulta, embora mais precisa em termos de espaço físico total ocupado no disco, só é realmente precisa ao fazer um
REBUILD
índice clusterizado. E depois disso, você ainda precisa levar em conta qualquerFILLFACTOR
configuração abaixo de 100. E mesmo assim sempre há cabeçalhos de página e, muitas vezes, uma certa quantidade de espaço "desperdiçado" que simplesmente não pode ser preenchido devido a ser muito pequeno para caber em qualquer linha neste tabela, ou pelo menos a linha que logicamente deveria ir nesse slot.Com relação à precisão da 2ª consulta para determinar o "uso de dados", parece mais justo retroceder os bytes do cabeçalho da página, pois eles não são uso de dados: são custos indiretos de negócios. Se houver 1 linha em uma página de dados e essa linha for apenas um
TINYINT
, esse 1 byte ainda exigirá que a página de dados exista e, portanto, os 96 bytes do cabeçalho. Esse 1 departamento deve ser cobrado por toda a página de dados? Se essa página de dados for preenchida pelo Departamento nº 2, eles dividiriam igualmente esse custo "overhead" ou pagariam proporcionalmente? Parece mais fácil simplesmente recuar. Nesse caso, usar um valor de8
para multiplicarnumber of pages
é muito alto. Que tal:Portanto, use algo como:
para todos os cálculos nas colunas "number_of_pages".
AND , considerando que o uso de
DATALENGTH
per each field não pode retornar os metadados por linha, que devem ser adicionados à sua consulta por tabela, onde você obtém oDATALENGTH
per each field, filtrando cada "departamento":ALLOW_SNAPSHOT_ISOLATION
definidoREAD_COMMITTED_SNAPSHOT
comoON
)NULL
, e se o valor couber na linha, ele pode ser muito menor ou muito maior que o ponteiro, e se o valor for armazenado fora linha, o tamanho do ponteiro pode depender da quantidade de dados existentes. 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 ;-). Isso é por cadaMAX
campo.Portanto, use algo como:
Em geral (cabeçalho de linha + número de colunas + array de slots + bitmap NULL):
Em geral (detecção automática se "informações da versão" estiver presente):
SE houver colunas de comprimento variável, adicione:
SE houver alguma
MAX
coluna /LOB, adicione:No geral:
Isso não é exato e, novamente, não funcionará se você tiver Row ou Page Compression ativado no Heap ou no Clustered Index, 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 sobre como as páginas de dados são dispostas e como
DATALENGTH
podem explicar as coisas que não gastamos muito tempo revisando a segunda consulta. Executei essa consulta em uma única tabela e, em seguida, comparei esses valores com o que estava sendo relatadosys.dm_db_database_page_allocations
e não eram os mesmos valores para o número de páginas. Em um palpite, removi as funções agregadas eGROUP BY
, e substituí aSELECT
lista pora.*, '---' AS [---], p.*
. E então ficou claro: as pessoas devem ter cuidado de onde, nessas interwebs obscuras, obtêm suas informações e scripts ;-). A segunda consulta postada na pergunta não está exatamente correta, especialmente para esta pergunta em particular.Problema menor: fora disso não faz muito sentido
GROUP BY rows
(e não ter essa coluna em uma função agregada), o JOIN entresys.allocation_units
esys.partitions
não é tecnicamente correto. Existem 3 tipos de Unidades de Alocação, e uma delas deve se UNIR a um campo diferente. Muitas vezes ,partition_id
ehobt_id
são os mesmos, portanto, pode nunca haver um problema, mas às vezes esses dois campos têm valores diferentes.Problema principal: a consulta usa o
used_pages
campo. Esse campo cobre todos os tipos de páginas: Dados, Índice, IAM, etc, tc. Existe outro campo mais apropriado para usar quando se trata apenas dos dados reais:data_pages
.Adaptei a 2ª consulta na Questão com os itens acima em mente e usando o tamanho da página de dados que confirma o cabeçalho da página. Também removi dois JOINs que eram desnecessários:
sys.schemas
(substituído por chamada paraSCHEMA_NAME()
) esys.indexes
(o índice clusterizado é sempreindex_id = 1
e temosindex_id
emsys.partitions
).Talvez esta seja uma resposta grunge, mas é isso que eu faria.
Portanto, DATALENGTH representa apenas 86% do total. Ainda é uma divisão muito representativa. A sobrecarga na excelente resposta de srutzky deve ter uma divisão bastante uniforme.
Eu usaria sua segunda consulta (páginas) para o total. E use o primeiro (datalength) para alocar a divisão. Muitos custos são alocados usando uma normalização.
E você deve considerar que uma resposta mais próxima aumentará o custo, portanto, mesmo o departamento que perdeu em uma divisão ainda pode pagar mais.