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

11

Fiquei com 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 essa consulta abaixo (que obtive on-line para obter tamanhos de tabela, apenas índices agrupados para que não inclua índices NC) para obter o tamanho de uma tabela específica no 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. Eu 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 variar bastante devido aos VARCHAR(MAX)campos na tabela, portanto, não posso apenas ter um tamanho médio * a proporção de linhas para um departamento. Quando uso a DATALENGTH()abordagem descrita acima, recebo 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 poder consultar diretamente o espaço usado por índice. Os índices filtrados podem ser criados programaticamente (e descartados novamente durante uma janela de manutenção ou quando eu precisar executar o faturamento periódico), em vez de usar o espaço o tempo todo (as partições seriam melhores nesse aspecto).

Eu gosto dessa sugestão e normalmente faria isso. Mas, para ser sincero, uso o "cada departamento" como exemplo para explicar por que preciso disso, mas para ser sincero, não é exatamente por isso. Devido a razões de confidencialidade, não consigo explicar exatamente o motivo pelo qual preciso desses dados, mas é análogo a diferentes departamentos.

Em relação aos índices não clusterizados nesta tabela: Se eu puder obter os tamanhos dos índices NC, isso seria ótimo. No entanto, os índices NC representam <1% do tamanho do índice clusterizado, portanto não podemos incluí-los. No entanto, como incluiríamos os índices NC de qualquer maneira? Não consigo nem obter um tamanho exato para o índice Clustered :)

Chris Woods
fonte
Portanto, em essência, você tem duas perguntas: (1) por que a soma do comprimento da linha não corresponde à contabilidade dos metadados do tamanho de toda a tabela? A resposta abaixo aborda isso, pelo menos em parte (e que pode variar por release e por recurso, por exemplo, compactação, columnstore, etc.). E mais importante: (2) como você pode determinar com precisão o espaço real usado por departamento? Não sei se você pode fazer isso com precisão - porque para alguns dos dados contidos na resposta, não há como saber a que departamento ele pertence.
Aaron Bertrand
Não acho que o problema seja que você não tenha um tamanho exato para o índice em cluster - os metadados definitivamente informam com precisão quanto espaço seu índice ocupa. O que os metadados não foram projetados para lhe dizer - pelo menos, devido ao seu design / estrutura atual - quanto dos dados estão associados a cada departamento.
Aaron Bertrand

Respostas:

19

                          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;
Solomon Rutzky
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Paul White 9
Embora a consulta atualizada que você forneceu para a 2ª consulta esteja ainda mais distante (na outra direção agora :)), estou de acordo com esta resposta. Aparentemente, essa é uma porcaria muito difícil de quebrar e, pelo que vale, fico feliz por os especialistas me ajudarem. Ainda não consegui descobrir o motivo exato pelos quais os dois métodos não são compatíveis. Vou usar apenas a metodologia na outra resposta para extrapolar. Eu gostaria de poder votar sim em ambas as respostas, mas @srutzky ajudou com todos os motivos pelos quais os dois estariam fora.
Chris Woods,
6

Talvez 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 (comprimento de dados) 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á os custos, de modo que mesmo o departamento que perdeu uma divisão ainda poderá pagar mais.

paparazzo
fonte