Rendimiento muy extraño con un índice XML

32

Mi pregunta se basa en esto: https://stackoverflow.com/q/35575990/5089204

Para dar una respuesta allí, hice el siguiente escenario de prueba.

Escenario de prueba

Primero creo una tabla de prueba y la lleno con 100.000 filas. Un número aleatorio (0 a 1000) debería conducir a ~ 100 filas para cada número aleatorio. Este número se coloca en una columna varchar y como valor en su XML.

Luego hago una llamada como el OP que necesita con .exist () y con .nodes () con una pequeña ventaja para el segundo, pero ambos toman de 5 a 6 segundos. De hecho, hago las llamadas dos veces: una segunda vez en orden intercambiado y con parámetros de búsqueda ligeramente modificados y con "// item" en lugar de la ruta completa para evitar falsos positivos a través de resultados o planes almacenados en caché.

Luego creo un índice XML y hago las mismas llamadas

Ahora, ¡lo que realmente me sorprendió! - la .nodescon ruta completa es mucho más lento que antes (9 segundos) pero el .exist()se ha reducido a la mitad de un segundo, con ruta completa incluso hasta aproximadamente 0,10 seg. (Mientras que .nodes()con un camino corto es mejor, pero aún muy lejos .exist())

Preguntas:

En resumen, mis propias pruebas se abren: los índices XML pueden hacer explotar una base de datos extremadamente. Pueden acelerar las cosas extremadamente (s. Edit 2), pero también pueden ralentizar sus consultas. Me gustaría entender cómo funcionan ... ¿Cuándo se debe crear un índice XML? ¿Por qué puede .nodes()ser peor con un índice que sin él? ¿Cómo podría uno evitar el 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;

EDITAR 1 - Resultados

Este es un resultado con SQL Server 2012 instalado localmente en una computadora portátil mediana. En esta prueba no pude reproducir el impacto extremadamente negativo en NodesFullPath_with_index, aunque es más lento que sin el í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

Prueba EDIT 2 con XML más grande

De acuerdo con la sugerencia de TT, utilicé el XML anterior, pero copié los itemnodos para alcanzar unos 450 elementos. Dejé que el nodo de hit esté muy arriba en el XML (porque creo que eso .exist()se detendría en el primer hit, mientras .nodes()continuaría)

La creación del índice XML hizo explotar el archivo mdf a ~ 21 GB, ~ 18 GB parecen pertenecer al í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
fuente

Respuestas:

33

Seguramente están pasando muchas cosas aquí, así que tendremos que ver a dónde lleva esto.

En primer lugar, la diferencia en el tiempo entre SQL Server 2012 y SQL Server 2014 se debe al nuevo estimador de cardinalidad en SQL Server 2014. Puede usar un indicador de rastreo en SQL Server 2014 para forzar el viejo estimador y luego verá el mismo tiempo características en SQL Server 2014 como en SQL Server 2012.

Comparar nodes()vs exist()no es justo ya que no devolverán el mismo resultado si hay más de un elemento coincidente en el XML para una fila. exist()sin embargo, devolverá una fila de la tabla base, mientras nodes()que potencialmente puede darle más de una fila devuelta por cada fila en la tabla base.
Conocemos los datos, pero SQL Server no y tiene que construir un plan de consulta que lo tenga en cuenta.

Para hacer que la nodes()consulta sea equivalente a la exist()consulta, puede hacer algo como esto.

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

Con una consulta como esa, no hay diferencia entre usar nodes()o exist()y eso se debe a que SQL Server crea casi el mismo plan para las dos versiones que no usan un índice y exactamente el mismo plan cuando se usa el índice. Eso es cierto tanto para SQL Server 2012 como para SQL Server 2014.

Para mí, en SQL Server 2012, las consultas sin el índice XML toman 6 segundos usando la versión modificada de la nodes()consulta anterior. No hay diferencia entre usar la ruta completa o la ruta corta. Con el índice XML en su lugar, la versión de ruta completa es la más rápida y tarda 5 ms, y el uso de la ruta corta tarda unos 500 ms. Examinar los planes de consulta le dirá por qué hay una diferencia, pero la versión corta es que cuando usa una ruta corta, SQL Server busca en el índice en la ruta corta (una búsqueda de rango usando like) y devuelve 700000 filas antes de descartar las filas que no coinciden en el valor. Al usar la ruta completa, SQL Server puede usar la expresión de ruta directamente junto con el valor del nodo para realizar la búsqueda y solo devuelve 105 filas desde cero para trabajar.

Con SQL Server 2014 y el nuevo estimador de cardinalidad, no hay diferencia en estas consultas cuando se usa un índice XML. Sin usar el índice, las consultas aún requieren la misma cantidad de tiempo, pero son 15 segundos. Claramente no es una mejora aquí cuando se usan cosas nuevas.

No estoy seguro de si perdí completamente la noción de qué se trata su pregunta, ya que modifiqué las consultas para que sean equivalentes, pero esto es lo que creo que es ahora.

¿Por qué la nodes()consulta (versión original) con un índice XML en su lugar es significativamente más lenta que cuando no se utiliza un índice?

Bueno, la respuesta es que el optimizador del plan de consultas de SQL Server hace algo malo y que está introduciendo un operador de cola. No sé por qué, pero la buena noticia es que ya no está allí con el nuevo estimador de cardinalidad en SQL Server 2014.
Sin índices establecidos, la consulta demora aproximadamente 7 segundos sin importar qué estimador de cardinalidad se use. Con el índice, lleva 15 segundos con el estimador anterior (SQL Server 2012) y aproximadamente 2 segundos con el estimador nuevo (SQL Server 2014).

Nota: Los hallazgos anteriores son válidos con sus datos de prueba. Puede haber una historia completamente diferente que contar si cambia el tamaño, la forma o la forma del XML. No hay forma de saberlo con certeza sin probar con los datos que realmente tiene en las tablas.

Cómo funcionan los índices XML

Los índices XML en SQL Server se implementan como tablas internas. El índice XML primario crea la tabla con la clave primaria de la tabla base más la columna de ID de nodo, en un total de 12 columnas. Tendrá una fila por cada element/node/attribute etc.tabla, por supuesto, puede crecer mucho dependiendo del tamaño del XML almacenado. Con un índice XML primario en su lugar, SQL Server puede usar la clave primaria de la tabla interna para localizar nodos y valores XML para cada fila en la tabla base.

Los índices XML secundarios vienen en tres tipos. Cuando crea un índice XML secundario, se crea un índice no agrupado en la tabla interna y, según el tipo de índice secundario que cree, tendrá diferentes columnas y órdenes de columnas.

Desde CREATE XML INDEX (Transact-SQL) :

VALOR
Crea un índice XML secundario en las columnas donde están las columnas clave (valor de nodo y ruta) del índice XML primario.

RUTA
Crea un índice XML secundario en columnas basadas en valores de ruta y valores de nodo en el índice XML primario. En el índice secundario PATH, la ruta y los valores de nodo son columnas clave que permiten búsquedas eficientes al buscar rutas.

PROPIEDAD
Crea un índice XML secundario en columnas (PK, ruta y valor de nodo) del índice XML primario donde PK es la clave principal de la tabla base.

Entonces, cuando crea un índice PATH, la primera columna en ese índice es la expresión de ruta y la segunda columna es el valor en ese nodo. En realidad, la ruta se almacena en una especie de formato comprimido y se invierte. Que se almacena al revés es lo que lo hace útil en las búsquedas que utilizan expresiones de ruta corta. En su caso de ruta corta que buscó //item/value/@string, //item/@namey //item. Dado que la ruta se almacena invertida en la columna, SQL Server puede usar una búsqueda de rango con like = '€€€€€€%dónde €€€€€€se invierte la ruta. Cuando usa una ruta completa, no hay razón para usarla, likeya que toda la ruta está codificada en la columna y el valor también puede usarse en el predicado de búsqueda.

Sus preguntas :

¿Cuándo se debe crear un índice XML?

Como último recurso si alguna vez. Es mejor diseñar su base de datos para que no tenga que usar valores dentro de XML para filtrar en una cláusula where. Si sabe de antemano que necesita hacer eso, puede usar la promoción de propiedades para crear una columna calculada que pueda indexar si es necesario. Desde SQL Server 2012 SP1, también tiene disponibles índices XML selectivos. El funcionamiento detrás de la escena es prácticamente el mismo que con los índices XML normales, solo usted especifica la expresión de ruta en la definición del índice y solo se indexan los nodos que coinciden. De esa manera puede ahorrar mucho espacio.

¿Por qué pueden ser .nodes () con un índice peor que sin él?

Cuando hay un índice XML creado en una tabla, SQL Server siempre usará ese índice (las tablas internas) para obtener los datos. Esa decisión se toma antes de que el optimizador pueda opinar sobre lo que es rápido y lo que no es rápido. La entrada al optimizador se reescribe para que use las tablas internas y luego depende del optimizador hacer lo mejor que pueda con una consulta regular. Cuando no se usa ningún índice, hay un par de funciones con valores de tabla que se usan en su lugar. La conclusión es que no se puede saber qué será más rápido sin realizar pruebas.

¿Cómo podría uno evitar el impacto negativo?

Pruebas

Mikael Eriksson
fuente
2
Sus ideas sobre la diferencia de .nodes()y .exist()son convincentes. Además, el hecho de que indexar full path searches más rápido parece fácil de entender. Esto significaría: Si crea un índice XML, debe siempre ser consciente de la influencia negativa con cualquier XPath genérico ( //o *o ..o [filter]o cualquier cosa no simplemente Xpath ...). De hecho, deberías usar solo la ruta completa, un gran retroceso ...
Shnugo