A presença do campo XML faz com que a maioria dos dados da tabela seja localizada nas páginas LOB_DATA (na verdade, ~ 90% das páginas da tabela são LOB_DATA).
Apenas ter a coluna XML na tabela não tem esse efeito. É a presença de dados XML que, sob certas condições , faz com que uma parte dos dados de uma linha seja armazenada fora da linha, nas páginas LOB_DATA. E enquanto um (ou talvez vários ;-) possa argumentar que sim, a XML
coluna implica que realmente haverá dados XML, não é garantido que os dados XML precisem ser armazenados fora da linha: a menos que a linha já esteja preenchida além de serem dados XML, documentos pequenos (até 8000 bytes) podem caber na linha e nunca vão para uma página LOB_DATA.
Estou correto ao pensar que as páginas LOB_DATA podem causar verificações lentas não apenas por causa de seu tamanho, mas também porque o SQL Server não pode verificar o índice clusterizado de maneira eficaz quando há muitas páginas LOB_DATA na tabela?
A digitalização refere-se à observação de todas as linhas. Obviamente, quando uma página de dados é lida, todos os dados em linha são lidos, mesmo se você selecionou um subconjunto das colunas. A diferença com os dados LOB é que, se você não selecionar essa coluna, os dados fora da linha não serão lidos. Portanto, não é realmente justo tirar uma conclusão sobre a eficiência com que o SQL Server pode varrer esse índice clusterizado, pois você não testou exatamente isso (ou testou metade dele). Você selecionou todas as colunas, que inclui a coluna XML e, como você mencionou, é onde a maioria dos dados está localizada.
Portanto, já sabemos que o SELECT TOP 1000 *
teste não estava apenas lendo uma série de 8k páginas de dados, todas seguidas, mas pulando para outros locais a cada linha . A estrutura exata desses dados LOB pode variar de acordo com o tamanho. Com base na pesquisa mostrada aqui ( Qual é o tamanho do ponteiro LOB para tipos (MAX) como Varchar, Varbinary, Etc? ), Existem dois tipos de alocações LOB fora da 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.
- 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 (por exemplo, " text_tree ").
Uma dessas duas situações ocorre cada vez que você recupera dados LOB com mais de 8000 bytes ou que simplesmente não se encaixam na linha. Publiquei um script de teste no PasteBin.com (script T-SQL para testar alocações e leituras de LOB ) que mostra os três tipos de alocações de LOB (com base no tamanho dos dados), bem como o efeito que cada um deles exerce sobre a lógica e leituras físicas. No seu caso, se os dados XML realmente forem inferiores a 42.000 bytes por linha, nenhum deles (ou muito pouco) deverá estar na estrutura TEXT_TREE menos eficiente.
Se você quiser testar a rapidez com que o SQL Server pode verificar esse Índice em Cluster, faça o, SELECT TOP 1000
mas especifique uma ou mais colunas que não incluem essa coluna XML. Como isso afeta seus resultados? Deve ser um pouco mais rápido.
é considerado razoável ter tal estrutura de tabela / padrão de dados?
Dado que temos uma descrição incompleta da estrutura da tabela e do padrão de dados reais, qualquer resposta pode não ser ideal, dependendo de quais são esses detalhes ausentes. Com isso em mente, eu diria que não há nada obviamente irracional na estrutura da tabela ou no padrão de dados.
Posso (no ac # app) compactar XML de 20 KB a ~ 2,5 KB e armazená-lo na coluna VARBINARY, impedindo o uso de páginas de dados LOB. Isso acelera os SELECTs 20 vezes nos meus testes.
Isso agilizou a seleção de todas as colunas, ou mesmo apenas os dados XML (agora em VARBINARY
), mas na verdade gera consultas que não selecionam os dados "XML". Supondo que você tenha cerca de 50 bytes nas outras colunas e um FILLFACTOR
de 100, então:
Sem compactação: 15k de XML
dados devem exigir 2 páginas LOB_DATA, que requerem 2 ponteiros para a raiz embutida. O primeiro ponteiro tem 24 bytes e o segundo é 12, para um total de 36 bytes armazenados em linha para os dados XML. O tamanho total da linha é 86 bytes e você pode ajustar cerca de 93 dessas linhas em uma página de dados de 8060 bytes. Portanto, 1 milhão de linhas requer 10.753 páginas de dados.
Compactação personalizada: 2,5 mil VARBINARY
dados caberão na linha. O tamanho total da linha é de 2610 (2,5 * 1024 = 2560) bytes e você pode ajustar apenas três dessas linhas em uma página de dados de 8060 bytes. Portanto, 1 milhão de linhas requer 333.334 páginas de dados.
Portanto, a implementação da compactação personalizada resulta em um aumento de 30x nas páginas de dados para o Clustered Index. Ou seja, todas as consultas que usam uma varredura de índice clusterizado agora têm cerca de 322.500 páginas a mais de dados para ler. Consulte a seção detalhada abaixo para obter ramificações adicionais ao fazer esse tipo de compactação.
Eu recomendaria não fazer refatoração com base no desempenho de SELECT TOP 1000 *
. Não é provável que seja uma consulta que o aplicativo emita e nem deve ser usada como a única base para otimizações potencialmente desnecessárias.
Para informações mais detalhadas e mais testes para tentar, consulte a seção abaixo.
Esta pergunta não pode receber uma resposta definitiva, mas podemos pelo menos progredir e sugerir pesquisas adicionais para ajudar a nos aproximar de descobrir o problema exato (idealmente com base em evidências).
O que nós sabemos:
- A tabela possui aproximadamente 1 milhão de linhas
- O tamanho da tabela é de aproximadamente 15 GB
- Tabela contém uma
XML
coluna e vários outros tipos de colunas: INT
, BIGINT
, UNIQUEIDENTIFIER
, "etc."
XML
coluna "tamanho" é, em média, aproximadamente 15k
- Após a execução
DBCC DROPCLEANBUFFERS
, leva de 20 a 25 segundos para que a seguinte consulta seja concluída:SELECT TOP 1000 * FROM TABLE
- O Índice de Cluster está sendo verificado
- A fragmentação no índice clusterizado é próxima de 0%
O que achamos que sabemos:
- Nenhuma outra atividade de disco fora dessas consultas. Você tem certeza? Mesmo se não houver outras consultas do usuário, existem operações em segundo plano? Existem processos externos ao SQL Server em execução na mesma máquina que poderiam estar ocupando parte do IO? Pode não haver, mas não está claro com base apenas nas informações fornecidas.
- 15 MB de dados XML estão sendo retornados. Em que esse número se baseia? Uma estimativa derivada das 1000 linhas vezes a média de 15k de dados XML por linha? Ou uma agregação programática do que foi recebido para essa consulta? Se for apenas uma estimativa, eu não confiaria nela, pois a distribuição dos dados XML pode não ser da maneira implícita por uma média simples.
A compactação XML pode ajudar. Como exatamente você faria a compactação no .NET? Através das classes GZipStream ou DeflateStream ? Esta não é uma opção de custo zero. Certamente compactará alguns dados em uma grande porcentagem, mas também exigirá mais CPU, pois você precisará de um processo adicional para compactar / descomprimir os dados a cada vez. Esse plano também removeria completamente sua capacidade de:
- consultar os dados XML através da
.nodes
, .value
, .query
, e .modify
funções XML.
indexe os dados XML.
Lembre-se (desde que você mencionou que o XML é "altamente redundante")XML
tipo de dados já está otimizado, pois armazena os nomes dos elementos e dos atributos em um dicionário, atribuindo um ID de índice inteiro a cada item e, em seguida, usando esse ID inteiro em todo o documento (portanto, ele não repete o nome completo para cada uso, nem o repete novamente como uma marca de fechamento para elementos). Os dados reais também têm espaço em branco externo removido. É por isso que os documentos XML extraídos não mantêm sua estrutura original e por que os elementos vazios são extraídos como <element />
se fossem inseridos como<element></element>
. Portanto, quaisquer ganhos de compactação via GZip (ou qualquer outra coisa) serão encontrados apenas pela compactação dos valores de elemento e / ou atributo, que é uma área de superfície muito menor que pode ser melhorada do que a maioria esperaria e provavelmente não vale a perda de recursos, conforme observado diretamente acima.
Lembre-se também de que a compactação dos dados XML e o armazenamento do VARBINARY(MAX)
resultado não eliminarão o acesso ao LOB, apenas o reduzirão. Dependendo do tamanho do restante dos dados na linha, o valor compactado pode caber na linha ou ainda pode exigir páginas LOB.
Essas informações, embora úteis, não são suficientes. Existem muitos fatores que influenciam o desempenho da consulta, portanto, precisamos de uma imagem muito mais detalhada do que está acontecendo.
O que não sabemos, mas precisamos:
- Por que o desempenho da
SELECT *
matéria? Esse é um padrão que você usa no código. Se sim, por quê?
- Qual é o desempenho de selecionar apenas a coluna XML? Quais são as estatísticas e o tempo se você fizer apenas
SELECT TOP 1000 XmlColumn FROM TABLE;
:?
Quanto dos 20 a 25 segundos necessários para retornar essas 1000 linhas está relacionado a fatores de rede (obtendo os dados pela conexão) e quanto está relacionado a fatores de cliente (renderizando aproximadamente 15 MB mais o restante dos Dados XML na grade no SSMS ou possivelmente salvando em disco)?
Fatorar esses dois aspectos da operação às vezes pode ser feito simplesmente não retornando os dados. Agora, pode-se pensar em selecionar uma Tabela Temporária ou Variável de Tabela, mas isso apenas introduziria algumas novas variáveis (por exemplo tempdb
, E / S de disco para , escritas no Log de Transações, possível crescimento automático de dados tempdb e / ou arquivo de log). espaço no buffer pool, etc). Todos esses novos fatores podem realmente aumentar o tempo de consulta. Em vez disso, normalmente armazeno as colunas em variáveis (do tipo de dados apropriado; não SQL_VARIANT
) que são substituídas a cada nova linha (ou seja SELECT @Column1 = tab.Column1,...
).
NO ENTANTO , conforme indicado por @PaulWhite neste DBA.StackExchange, a Logical lê diferentes ao acessar os mesmos dados LOB , com pesquisas adicionais publicadas no PasteBin ( script T-SQL para testar vários cenários para leituras LOB ) , LOBs não são acessados de forma consistente entre SELECT
, SELECT INTO
, SELECT @XmlVariable = XmlColumn
, SELECT @XmlVariable = XmlColumn.query(N'/')
, e SELECT @NVarCharVariable = CONVERT(NVARCHAR(MAX), XmlColumn)
. Portanto, nossas opções são um pouco mais limitadas aqui, mas aqui está o que pode ser feito:
- Elimine os problemas de rede executando a consulta no servidor que executa o SQL Server, no SSMS ou no SQLCMD.EXE.
- Para descartar problemas do cliente no SSMS, vá em Opções de consulta -> Resultados -> Grade e verifique a opção "Descartar resultados após a execução". Observe que esta opção impedirá TODAS as saídas, incluindo mensagens, mas ainda pode ser útil para descartar o tempo que o SSMS leva para alocar a memória por cada linha e desenhá-la na grade.
Alternativamente, você pode executar a consulta via sqlcmd.exe e direcionar a saída para ir para lugar nenhum via: -o NUL:
.
- Existe um tipo de espera associado a esta consulta? Se sim, o que é esse tipo de espera?
Qual é o tamanho real dos dados para as XML
colunas que estão sendo retornadas ? O tamanho médio dessa coluna em toda a tabela não importa realmente se as linhas "TOP 1000" contêm uma parte desproporcionalmente grande do total de XML
dados. Se você deseja conhecer as TOP 1000 linhas, consulte essas linhas. Por favor, execute o seguinte:
SELECT TOP 1000 tab.*,
SUM(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [TotalXmlKBytes],
AVG(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [AverageXmlKBytes]
STDEV(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [StandardDeviationForXmlKBytes]
FROM SchemaName.TableName tab;
- O esquema exato da tabela. Forneça a declaração completa
CREATE TABLE
, incluindo todos os índices.
- Plano de consulta? Isso é algo que você pode postar? Essa informação provavelmente não mudará nada, mas é melhor saber que não mudará do que adivinhar que não mudará e estará errado ;-)
- Existe fragmentação física / externa no arquivo de dados? Embora isso possa não ser um fator importante aqui, já que você está usando "SATA de nível de consumidor" e não SSD ou até mesmo SATA super-caro, o efeito de setores com ordem sub-ideal será mais perceptível, especialmente como o número desses setores isso precisa ser lido aumenta.
Quais são os resultados exatos da seguinte consulta:
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),
OBJECT_ID(N'dbo.SchemaName.TableName'), 1, 0, N'LIMITED');
ATUALIZAR
Ocorreu-me que eu deveria tentar reproduzir esse cenário para ver se tenho um comportamento semelhante. Então, criei uma tabela com várias colunas (semelhante à vaga descrição da Pergunta) e a preenchi com 1 milhão de linhas, e a coluna XML possui aproximadamente 15k de dados por linha (veja o código abaixo).
O que eu descobri é que fazer um procedimento SELECT TOP 1000 * FROM TABLE
concluído em 8 segundos na primeira vez e em 2 a 4 segundos a cada momento (sim, executando DBCC DROPCLEANBUFFERS
antes de cada execução da SELECT *
consulta). E meu laptop de vários anos não é rápido: SQL Server 2012 SP2 Developer Edition, 64 bits, 6 GB de RAM, dual 2.5 Ghz Core i5 e uma unidade SATA de 5400 RPM. Também estou executando o SSMS 2014, SQL Server Express 2014, Chrome e várias outras coisas.
Com base no tempo de resposta do meu sistema, repetirei que precisamos de mais informações (ou seja, informações específicas sobre a tabela e os dados, resultados dos testes sugeridos etc.) para ajudar a diminuir a causa do tempo de resposta de 20 a 25 segundos que você está vendo.
SET ANSI_NULLS, NOCOUNT ON;
GO
IF (OBJECT_ID(N'dbo.XmlReadTest') IS NOT NULL)
BEGIN
PRINT N'Dropping table...';
DROP TABLE dbo.XmlReadTest;
END;
PRINT N'Creating table...';
CREATE TABLE dbo.XmlReadTest
(
ID INT NOT NULL IDENTITY(1, 1),
Col2 BIGINT,
Col3 UNIQUEIDENTIFIER,
Col4 DATETIME,
Col5 XML,
CONSTRAINT [PK_XmlReadTest] PRIMARY KEY CLUSTERED ([ID])
);
GO
DECLARE @MaxSets INT = 1000,
@CurrentSet INT = 1;
WHILE (@CurrentSet <= @MaxSets)
BEGIN
RAISERROR(N'Populating data (1000 sets of 1000 rows); Set # %d ...',
10, 1, @CurrentSet) WITH NOWAIT;
INSERT INTO dbo.XmlReadTest (Col2, Col3, Col4, Col5)
SELECT TOP 1000
CONVERT(BIGINT, CRYPT_GEN_RANDOM(8)),
NEWID(),
GETDATE(),
N'<test>'
+ REPLICATE(CONVERT(NVARCHAR(MAX), CRYPT_GEN_RANDOM(1), 2), 3750)
+ N'</test>'
FROM [master].[sys].all_columns sac1;
IF ((@CurrentSet % 100) = 0)
BEGIN
RAISERROR(N'Executing CHECKPOINT ...', 10, 1) WITH NOWAIT;
CHECKPOINT;
END;
SET @CurrentSet += 1;
END;
--
SELECT COUNT(*) FROM dbo.XmlReadTest; -- Verify that we have 1 million rows
-- O.P. states that the "clustered index fragmentation is close to 0%"
ALTER INDEX [PK_XmlReadTest] ON dbo.XmlReadTest REBUILD WITH (FILLFACTOR = 90);
CHECKPOINT;
--
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 * FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 5676, lob physical reads 1, lob read-ahead reads 3967.
SQL Server Execution Times:
CPU time = 171 ms, elapsed time = 8329 ms.
*/
E, como queremos calcular o tempo necessário para ler as páginas que não são do LOB, executei a consulta a seguir para selecionar tudo, exceto a coluna XML (um dos testes sugeridos acima). Isso retorna em 1,5 segundos de forma bastante consistente.
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 ID, Col2, Col3, Col4 FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1666 ms.
*/
Conclusão (no momento)
Com base na minha tentativa de recriar seu cenário, acho que não podemos apontar a unidade SATA ou a E / S não sequencial como a principal causa dos 20 - 25 segundos, especialmente porque ainda estamos não sabe a rapidez com que a consulta retorna quando não inclui a coluna XML. E não pude reproduzir o grande número de leituras lógicas (não LOB) que você está mostrando, mas tenho a sensação de que preciso adicionar mais dados a cada linha à luz disso e da declaração de:
~ 90% das páginas da tabela são LOB_DATA
Minha tabela possui 1 milhão de linhas, cada uma com pouco mais de sys.dm_db_index_physical_stats
15 mil dados XML e mostra que existem 2 milhões de páginas LOB_DATA. Os 10% restantes seriam 222k IN_ROW páginas de dados, mas eu tenho apenas 11.630 delas. Portanto, mais uma vez, precisamos de mais informações sobre o esquema da tabela e os dados reais.