Desempenho muito estranho com um índice XML

32

Minha pergunta é baseada nisso: https://stackoverflow.com/q/35575990/5089204

Para dar uma resposta lá, fiz o seguinte cenário de teste.

Cenário de teste

Primeiro, crio uma tabela de teste e a preenche com 100.000 linhas. Um número aleatório (0 a 1000) deve levar a ~ 100 linhas para cada número aleatório. Esse número é colocado em uma coluna varchar e como um valor em seu XML.

Então eu faço uma chamada como o OP que precisa com .exist () e .nodes () com uma pequena vantagem para o segundo, mas ambos levam de 5 a 6 segundos. Na verdade, eu faço as chamadas duas vezes: uma segunda vez em ordem trocada e com parâmetros de pesquisa ligeiramente alterados e com "// item" em vez do caminho completo para evitar falsos positivos por meio de resultados ou planos em cache.

Então eu crio um índice XML e faço as mesmas chamadas

Agora - o que realmente me surpreendeu! - o .nodescom caminho completo é muito mais lento do que antes (9 segundos), mas o .exist()é para baixo a metade de um segundo, com caminho completo mesmo para baixo a cerca de 0,10 segundos. (enquanto .nodes()com caminho curto é melhor, mas ainda muito atrás .exist())

Questões:

Meus próprios testes são resumidos: índices XML podem explodir extremamente um banco de dados. Eles podem acelerar as coisas extremamente (s. Ed. 2), mas também podem atrasar suas consultas. Gostaria de entender como eles funcionam ... Quando alguém deve criar um índice XML? Por que .nodes()com um índice pode ser pior do que sem? Como alguém poderia evitar o impacto negativo?

CREATE TABLE #testTbl(ID INT IDENTITY PRIMARY KEY, SomeData VARCHAR(100),XmlColumn XML);
GO

DECLARE @RndNumber VARCHAR(100)=(SELECT CAST(CAST(RAND()*1000 AS INT) AS VARCHAR(100)));

INSERT INTO #testTbl VALUES('Data_' + @RndNumber,
'<error application="application" host="host" type="exception" message="message" >
  <serverVariables>
    <item name="name1">
      <value string="text" />
    </item>
    <item name="name2">
      <value string="text2" />
    </item>
    <item name="name3">
      <value string="text3" />
    </item>
    <item name="name4">
      <value string="text4" />
    </item>
    <item name="name5">
      <value string="My test ' +  @RndNumber + '" />
    </item>
    <item name="name6">
      <value string="text6" />
    </item>
    <item name="name7">
      <value string="text7" />
    </item>
  </serverVariables>
</error>');

GO 100000

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesFullPath_no_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistFullPath_no_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('//item[@name="name5" and value/@string="My test 500"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistShortPath_no_index;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('//item[@name="name5" and value/@string="My test 500"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesShortPath_no_index;
GO

CREATE PRIMARY XML INDEX PXML_test_XmlColum1 ON #testTbl(XmlColumn);
CREATE XML INDEX IXML_test_XmlColumn2 ON #testTbl(XmlColumn) USING XML INDEX PXML_test_XmlColum1 FOR PATH;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesFullPath_with_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistFullPath_with_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('//item[@name="name5" and value/@string="My test 500"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistShortPath_with_index;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('//item[@name="name5" and value/@string="My test 500"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesShortPath_with_index;
GO

DROP TABLE #testTbl;

EDIT 1 - Resultados

Este é um resultado com o SQL Server 2012 instalado localmente em um laptop médio. Neste teste, não pude reproduzir o impacto extremamente negativo NodesFullPath_with_index, embora seja mais lento do que sem o índice ...

NodesFullPath_no_index    6.067
ExistFullPath_no_index    6.223
ExistShortPath_no_index   8.373
NodesShortPath_no_index   6.733

NodesFullPath_with_index  7.247
ExistFullPath_with_index  0.217
ExistShortPath_with_index 0.500
NodesShortPath_with_index 2.410

EDIT 2 Teste com XML maior

De acordo com a sugestão do TT, usei o XML acima, mas copiei os item-nodes para alcançar cerca de 450 itens. Eu deixei o nó da ocorrência estar muito alto no XML (porque acho que isso .exist()iria parar na primeira ocorrência, enquanto .nodes()continuaria)

A criação do índice XML expandiu o arquivo mdf para ~ 21GB, ~ 18GB parecem pertencer ao índice (!!!)

NodesFullPath_no_index    3min44
ExistFullPath_no_index    3min39
ExistShortPath_no_index   3min49
NodesShortPath_no_index   4min00

NodesFullPath_with_index  8min20
ExistFullPath_with_index  8,5 seconds !!!
ExistShortPath_with_index 1min21
NodesShortPath_with_index 13min41 !!!
Shnugo
fonte

Respostas:

33

Há muita coisa acontecendo aqui, então teremos que ver aonde isso leva.

Primeiro, a diferença de tempo entre o SQL Server 2012 e o SQL Server 2014 se deve ao novo estimador de cardinalidade no SQL Server 2014. Você pode usar um sinalizador de rastreamento no SQL Server 2014 para forçar o estimador antigo e verá o mesmo tempo características no SQL Server 2014 como no SQL Server 2012.

Comparar nodes()vs exist()não é justo, pois eles não retornarão o mesmo resultado se houver mais de um elemento correspondente no XML para uma linha. exist()retornará uma linha da tabela base independentemente, enquanto nodes()potencialmente poderá fornecer mais de uma linha retornada para cada linha na tabela base.
Conhecemos os dados, mas o SQL Server não e precisa criar um plano de consulta que leve isso em consideração.

Para tornar a nodes()consulta equivalente à exist()consulta, você pode fazer algo assim.

SELECT testTbl.*
FROM testTbl
WHERE EXISTS (
             SELECT *
             FROM XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b)
             )

Com uma consulta como essa, não há diferença entre usar nodes()or exist()e isso ocorre porque o SQL Server cria quase o mesmo plano para as duas versões que não usam um índice e exatamente o mesmo plano quando o índice é usado. Isso é verdade para o SQL Server 2012 e o SQL Server 2014.

Para mim, no SQL Server 2012, as consultas sem o índice XML levam 6 segundos usando a versão modificada da nodes()consulta acima. Não há diferença entre usar o caminho completo ou o caminho curto. Com o índice XML em vigor, a versão do caminho completo é a mais rápida e leva 5 ms e o uso do caminho curto leva cerca de 500 ms. Examinar os planos de consulta mostrará por que existe uma diferença, mas a versão curta é que, quando você usa um caminho curto, o SQL Server procura no índice no caminho curto (um intervalo que procura usar like) e retorna 700000 linhas antes de descartar as linhas que não corresponde ao valor. Ao usar o caminho completo, o SQL Server pode usar a expressão do caminho diretamente junto com o valor do nó para fazer a busca e retorna apenas 105 linhas do zero para trabalhar.

Usando o SQL Server 2014 e o novo estimador de cardinalidade, não há diferença nessas consultas ao usar um índice XML. Sem usar o índice, as consultas ainda levam a mesma quantidade de tempo, mas são 15 segundos. Claramente, não é uma melhoria aqui ao usar coisas novas.

Não tenho certeza se perdi completamente a noção da sua pergunta, pois modifiquei as consultas para serem equivalentes, mas aqui está o que acredito que é agora.

Por que a nodes()consulta (versão original) com um índice XML no local é significativamente mais lenta que quando um índice não é usado?

Bem, a resposta é que o otimizador de plano de consulta do SQL Server faz algo ruim e está introduzindo um operador de spool. Não sei por que, mas a boa notícia é que ele não existe mais com o novo estimador de cardinalidade no SQL Server 2014.
Sem índices em vigor, a consulta leva cerca de 7 segundos, independentemente do estimador de cardinalidade usado. Com o índice, leva 15 segundos com o antigo estimador (SQL Server 2012) e cerca de 2 segundos com o novo estimador (SQL Server 2014).

Nota: As descobertas acima são válidas com seus dados de teste. Pode haver uma história totalmente diferente para contar se você altera o tamanho, a forma ou a forma do XML. Não há como ter certeza sem testar com os dados que você realmente possui nas tabelas.

Como os índices XML funcionam

Os índices XML no SQL Server são implementados como tabelas internas. O índice XML primário cria a tabela com a chave primária da tabela base mais a coluna id do nó, no total 12 colunas. Ele terá uma linha por vez, element/node/attribute etc.para que a tabela possa ficar muito grande, dependendo do tamanho do XML armazenado. Com um índice XML primário em vigor, o SQL Server pode usar a chave primária da tabela interna para localizar nós e valores XML para cada linha na tabela base.

Os índices XML secundários vêm em três tipos. Quando você cria um índice XML secundário, existe um índice não agrupado em cluster na tabela interna e, dependendo do tipo de índice secundário criado, ele terá colunas e ordens de colunas diferentes.

Em CREATE XML INDEX (Transact-SQL) :

VALUE
Cria um índice XML secundário em colunas onde as colunas principais são (valor e caminho do nó) do índice XML primário.

PATH
Cria um índice XML secundário em colunas construídas em valores de caminho e valores de nó no índice XML primário. No índice secundário PATH, os valores de caminho e nó são colunas-chave que permitem buscas eficientes ao procurar caminhos.

PROPERTY
Cria um índice XML secundário nas colunas (PK, caminho e valor do nó) do índice XML primário em que PK é a chave primária da tabela base.

Portanto, quando você cria um índice PATH, a primeira coluna nesse índice é a expressão do caminho e a segunda coluna é o valor nesse nó. Na verdade, o caminho é armazenado em um tipo de formato compactado e revertido. O armazenamento invertido é o que o torna útil em pesquisas usando expressões de caminho curto. No seu caso curto caminho que você procurou //item/value/@string, //item/@namee //item. Como o caminho é armazenado invertido na coluna, o SQL Server pode usar uma pesquisa de intervalo com like = '€€€€€€%onde €€€€€€está o caminho invertido. Quando você usa um caminho completo, não há motivo para usá-lo, likepois o caminho inteiro é codificado na coluna e o valor também pode ser usado no predicado de busca.

Suas perguntas :

Quando alguém deve criar um índice XML?

Como último recurso, se alguma vez. Melhor projetar seu banco de dados para que você não precise usar valores dentro do XML para filtrar na cláusula where. Se você souber de antemão que precisa fazer isso, poderá usar a promoção de propriedades para criar uma coluna computada que poderá indexar, se necessário. Desde o SQL Server 2012 SP1, você também tem índices XML seletivos disponíveis. Os trabalhos por trás da cena são praticamente os mesmos dos índices XML regulares, apenas você especifica a expressão do caminho na definição de índice e apenas os nós correspondentes são indexados. Dessa forma, você pode economizar muito espaço.

Por que .nodes () com um índice pode ser pior do que sem?

Quando houver um índice XML criado em uma tabela, o SQL Server sempre usará esse índice (as tabelas internas) para obter os dados. Essa decisão é tomada antes que o otimizador possa dizer o que é rápido e o que não é rápido. A entrada para o otimizador é reescrita para que você use as tabelas internas e, depois disso, cabe ao otimizador fazer o melhor possível com uma consulta regular. Quando nenhum índice é usado, existem algumas funções com valor de tabela que são usadas. O ponto principal é que você não pode dizer o que será mais rápido sem testar.

Como alguém pode evitar o impacto negativo?

Teste

Mikael Eriksson
fonte
2
Suas idéias sobre a diferença .nodes()e .exist()são convincentes. Também full path searché fácil entender o fato de que o índice com é mais rápido. Isso significaria: Se você criar um índice XML, sempre deve estar ciente da influência negativa de qualquer XPath genérico ( //ou *ou ..ou [filter]ou qualquer coisa que não seja simplesmente o Xpath simples ...). Na verdade, você deve usar o caminho completo única - muito grande de volta desenhar ...
Shnugo