sql >> Base de Datos >  >> RDS >> Sqlserver

STRING_SPLIT() en SQL Server 2016:Seguimiento #1

Hace unas semanas, escribí sobre lo sorprendido que estaba por el rendimiento de una nueva función nativa en SQL Server 2016, STRING_SPLIT() :

  • Sorpresas y suposiciones de rendimiento:STRING_SPLIT()

Después de que se publicó la publicación, recibí algunos comentarios (públicos y privados) con estas sugerencias (o preguntas que convertí en sugerencias):

  • Especificar un tipo de datos de salida explícito para el enfoque JSON, de modo que ese método no sufra una posible sobrecarga de rendimiento debido al respaldo de nvarchar(max) .
  • Probar un enfoque ligeramente diferente, en el que realmente se hace algo con los datos, a saber, SELECT INTO #temp .
  • Mostrar cómo se comparan los recuentos de filas estimados con los métodos existentes, especialmente cuando se anidan operaciones de división.

Respondí a algunas personas sin conexión, pero pensé que valdría la pena publicar un seguimiento aquí.

Ser más justos con JSON

La función JSON original se veía así, sin especificación para el tipo de datos de salida:

CREATE FUNCTION dbo.SplitStrings_JSON
...
    RETURN (SELECT value FROM OPENJSON( CHAR(91) + @List + CHAR(93) ));

Le cambié el nombre y creé dos más, con las siguientes definiciones:

CREATE FUNCTION dbo.SplitStrings_JSON_int
...
    RETURN (SELECT value FROM OPENJSON( CHAR(91) + @List + CHAR(93) ) 
      WITH ([value] int '$'));
GO
 
CREATE FUNCTION dbo.SplitStrings_JSON_varchar
...
    RETURN (SELECT value FROM OPENJSON( CHAR(91) + @List + CHAR(93) ) 
      WITH ([value] varchar(100) '$'));

Pensé que esto mejoraría drásticamente el rendimiento, pero lamentablemente, este no fue el caso. Realicé las pruebas nuevamente y los resultados fueron los siguientes:

Las esperas observadas durante una instancia aleatoria de la prueba (filtradas a> 25):

CLR IO_COMPLETION 1595
SOS_SCHEDULER_YIELD 76
RESERVADO_MEMORIA_ALLOCATION_EXT 76
MEMORY_ALLOCATION_EXT 28
JSON_int MEMORY_ALLOCATION_EXT 6294
SOS_SCHEDULER_YIELD 95
JSON_original MEMORY_ALLOCATION_EXT 4307
SOS_SCHEDULER_YIELD 83
JSON_varchar MEMORY_ALLOCATION_EXT 6110
SOS_SCHEDULER_YIELD 87
Números SOS_SCHEDULER_YIELD 96
XML MEMORY_ALLOCATION_EXT 1917
IO_COMPLETION 1616
SOS_SCHEDULER_YIELD 147
RESERVADO_MEMORIA_ALLOCATION_EXT 73

Esperas observadas> 25 (tenga en cuenta que no hay ninguna entrada para STRING_SPLIT )

Al cambiar del valor predeterminado a varchar(100) mejoró un poco el rendimiento, la ganancia fue insignificante y cambió a int en realidad lo empeoró. Agregue a esto que probablemente necesite agregar STRING_ESCAPE() a la cadena entrante en algunos escenarios, en caso de que tengan caracteres que arruinen el análisis de JSON. Mi conclusión sigue siendo que esta es una buena manera de usar la nueva funcionalidad JSON, pero sobre todo una novedad inapropiada para una escala razonable.

Materializar el resultado

Jonathan Magnan hizo esta astuta observación en mi publicación anterior:

STRING_SPLIT es realmente muy rápido, sin embargo, también es muy lento cuando se trabaja con una tabla temporal (a menos que se arregle en una compilación futura).

SELECT f.value
INTO #test
FROM dbo.SourceTable AS s
CROSS APPLY string_split(s.StringValue, ',') AS f

Será MUCHO más lento que la solución SQL CLR (¡15 veces y más!).

Entonces, profundicé. Creé un código que llamaría a cada una de mis funciones y volcaría los resultados en una tabla #temp, y los cronometraría:

SET NOCOUNT ON;
 
SELECT N'SET NOCOUNT ON;
TRUNCATE TABLE dbo.Timings;
GO
';
 
SELECT N'DECLARE @d DATETIME = SYSDATETIME();
 
INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)
SELECT @d, test = ''' + name + ''', point  = ''Start'', wait_type, wait_time_ms
FROM sys.dm_exec_session_wait_stats WHERE session_id = @@SPID;
GO
 
SELECT f.value
INTO #test
FROM dbo.SourceTable AS s
CROSS APPLY dbo.'+name+'(s.StringValue, '','') AS f;
GO
 
DECLARE @d DATETIME = SYSDATETIME();
 
INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)
SELECT @d, '''+name+''', ''End'', wait_type, wait_time_ms
FROM sys.dm_exec_session_wait_stats WHERE session_id = @@SPID;
 
DROP TABLE #test;
GO'
FROM sys.objects WHERE name LIKE '%split%';

Solo ejecuté cada prueba una vez (en lugar de hacer un bucle 100 veces), porque no quería destrozar por completo la E/S de mi sistema. Aún así, después de promediar tres ejecuciones de prueba, Jonathan estaba absolutamente en lo correcto al 100%. Estas fueron las duraciones de llenar una tabla #temp con ~500,000 filas usando cada método:

Así que aquí, el JSON y STRING_SPLIT los métodos tardaron unos 10 segundos cada uno, mientras que los enfoques de tabla de números, CLR y XML tardaron menos de un segundo. Perplejo, investigué las esperas y, efectivamente, los cuatro métodos de la izquierda incurrieron en LATCH_EX significativos. esperas (alrededor de 25 segundos) que no se ven en los otros tres, y no hubo otras esperas significativas de las que hablar.

Y dado que las esperas del pestillo fueron mayores que la duración total, me dio una pista de que esto tenía que ver con el paralelismo (esta máquina en particular tiene 4 núcleos). Así que generé código de prueba nuevamente, cambiando solo una línea para ver qué pasaría sin paralelismo:

CROSS APPLY dbo.'+name+'(s.StringValue, '','') AS f OPTION (MAXDOP 1);

Ahora STRING_SPLIT le fue mucho mejor (al igual que los métodos JSON), pero al menos duplicó el tiempo que tomó CLR:

Por lo tanto, podría haber un problema pendiente en estos nuevos métodos cuando se trata de paralelismo. No fue un problema de distribución de subprocesos (lo verifiqué), y CLR en realidad tenía peores estimaciones (100x real frente a solo 5x para STRING_SPLIT ); solo algún problema subyacente con la coordinación de pestillos entre hilos, supongo. Por ahora, podría valer la pena usar MAXDOP 1 si sabe que está escribiendo el resultado en nuevas páginas.

He incluido los planes gráficos que comparan el enfoque CLR con el nativo, tanto para la ejecución en paralelo como en serie (también he subido un archivo de análisis de consulta que puede abrir en SQL Sentry Plan Explorer para husmear por su cuenta):

STRING_SPLIT

CLR

La advertencia de clasificación, FYI, no fue demasiado impactante y, obviamente, no tuvo un efecto tangible en la duración de la consulta:

  • StringSplit.queryanalysis.zip (25kb)

Se prepara para el verano

Cuando miré un poco más de cerca esos planes, noté que en el plan CLR, hay un carrete perezoso. Esto se introduce para asegurarse de que los duplicados se procesen juntos (para ahorrar trabajo al hacer menos divisiones reales), pero este carrete no siempre es posible en todas las formas del plan, y puede brindar cierta ventaja a aquellos que pueden usarlo ( por ejemplo, el plan CLR), dependiendo de las estimaciones. Para comparar sin carretes, habilité el indicador de seguimiento 8690 y ejecuté las pruebas nuevamente. Primero, aquí está el plan CLR paralelo sin el carrete:

Y aquí estaban las nuevas duraciones para todas las consultas paralelas con TF 8690 habilitado:

Ahora, aquí está el plan CLR serial sin el carrete:

Y estos fueron los resultados de tiempo para las consultas que utilizan TF 8690 y MAXDOP 1 :

(Tenga en cuenta que, además del plan XML, la mayoría de los demás no cambiaron en absoluto, con o sin la marca de rastreo).

Comparación de recuentos de filas estimados

Dan Holmes hizo la siguiente pregunta:

¿Cómo estima el tamaño de los datos cuando se une a otra función dividida (o múltiple)? El siguiente enlace es una redacción de una implementación dividida basada en CLR. ¿El 2016 hace un trabajo 'mejor' con las estimaciones de datos? (lamentablemente, todavía no tengo la capacidad de instalar RC).

http://sql.dnhlms.com/2016/02/sql-clr-based-string-splitting-and. html

Entonces, deslicé el código de la publicación de Dan, lo cambié para usar mis funciones y lo ejecuté a través de Plan Explorer:

DECLARE @s VARCHAR(MAX);
 
SELECT * FROM dbo.SplitStrings_CLR(@s, ',') s
CROSS APPLY   dbo.SplitStrings_CLR(s.value, ';') s1 
CROSS APPLY   dbo.SplitStrings_CLR(s1.value, '!') s2
CROSS APPLY   dbo.SplitStrings_CLR(s2.value, '#') s3;
 
SELECT * FROM dbo.SplitStrings_Numbers(@s, ',') s
CROSS APPLY   dbo.SplitStrings_Numbers(s.value, ';') s1 
CROSS APPLY   dbo.SplitStrings_Numbers(s1.value, '!') s2
CROSS APPLY   dbo.SplitStrings_Numbers(s2.value, '#') s3;
 
SELECT * FROM dbo.SplitStrings_Native(@s, ',') s
CROSS APPLY   dbo.SplitStrings_Native(s.value, ';') s1 
CROSS APPLY   dbo.SplitStrings_Native(s1.value, '!') s2
CROSS APPLY   dbo.SplitStrings_Native(s2.value, '#') s3;

El SPLIT_STRING El enfoque ciertamente genera estimaciones *mejores* que CLR, pero aún así muy por encima (en este caso, cuando la cadena está vacía; este podría no ser siempre el caso). La función tiene un valor predeterminado incorporado que estima que la cadena entrante tendrá 50 elementos, por lo que cuando los anida obtiene 50 x 50 (2500); si los anida de nuevo, 50 x 2500 (125 000); y finalmente, 50 x 125 000 (6 250 000):

Nota:OPENJSON() se comporta exactamente de la misma manera que STRING_SPLIT – también asume que saldrán 50 filas de cualquier operación de división dada. Estoy pensando que podría ser útil tener una forma de insinuar cardinalidad para funciones como esta, además de marcas de seguimiento como 4137 (anterior a 2014), 9471 y 9472 (2014+) y, por supuesto, 9481...

Esta estimación de 6,25 millones de filas no es excelente, pero es mucho mejor que el enfoque CLR del que hablaba Dan, que estima UN TRILLÓN DE FILAS , y perdí la cuenta de las comas para determinar el tamaño de los datos:¿16 petabytes? exabytes?

Obviamente, a algunos de los otros enfoques les va mejor en términos de estimaciones. La tabla Numbers, por ejemplo, estimó 438 filas mucho más razonables (en SQL Server 2016 RC2). ¿De dónde viene este número? Bueno, hay 8000 filas en la tabla, y si recuerdas, la función tiene un predicado de igualdad y desigualdad:

     WHERE Number <= LEN(@List)
     AND SUBSTRING(@Delimiter + @List, [Number], 1) = @Delimiter

Entonces, SQL Server multiplica el número de filas en la tabla por 10% (como una conjetura) para el filtro de igualdad, luego la raíz cuadrada del 30% (de nuevo, una conjetura) para el filtro de desigualdad. La raíz cuadrada se debe al retroceso exponencial, que Paul White explica aquí. Esto nos da:

8000 * 0,1 * SQRT(0,3) =438,178

La variación de XML se estimó en poco más de mil millones de filas (debido a un spool de tabla que se estima que se ejecutará 5,8 millones de veces), pero su plan era demasiado complejo para tratar de ilustrarlo aquí. En cualquier caso, recuerde que las estimaciones claramente no cuentan la historia completa:el hecho de que una consulta tenga estimaciones más precisas no significa que funcionará mejor.

Había algunas otras formas en las que podía modificar un poco las estimaciones:a saber, forzando el antiguo modelo de estimación de cardinalidad (que afectaba tanto a las variaciones de la tabla XML como a la de Numbers) y usando los TF 9471 y 9472 (que afectaban solo a la variación de la tabla de Numbers, ya que ambos controlan la cardinalidad en torno a múltiples predicados). Estas eran las formas en que podía cambiar las estimaciones solo un poco (o MUCHO , en el caso de volver al antiguo modelo CE):

El antiguo modelo CE redujo las estimaciones de XML en un orden de magnitud, pero para la tabla Numbers, la explotó por completo. Los indicadores de predicado alteraron las estimaciones de la tabla Números, pero esos cambios son mucho menos interesantes.

Ninguna de estas marcas de seguimiento tuvo ningún efecto en las estimaciones de CLR, JSON o STRING_SPLIT variaciones.

Conclusión

Entonces, ¿qué aprendí aquí? Un montón, en realidad:

  • El paralelismo puede ayudar en algunos casos, pero cuando no ayuda, realmente no ayuda Los métodos JSON eran ~5 veces más rápidos sin paralelismo y STRING_SPLIT fue casi 10 veces más rápido.
  • El spool realmente ayudó a que el enfoque CLR funcionara mejor en este caso, pero TF 8690 podría ser útil para experimentar en otros casos en los que está viendo spools y está tratando de mejorar el rendimiento. Estoy seguro de que hay situaciones en las que eliminar el carrete terminará siendo mejor en general.
  • Eliminar el spool realmente perjudicó el enfoque XML (pero solo drásticamente cuando se vio obligado a ser de un solo subproceso).
  • Pueden suceder muchas cosas raras con las estimaciones según el enfoque, junto con las estadísticas, la distribución y las marcas de seguimiento habituales. Bueno, supongo que ya lo sabía, pero definitivamente hay un par de buenos ejemplos tangibles aquí.

Gracias a las personas que hicieron preguntas o me empujaron a incluir más información. Y como habrás adivinado por el título, abordé otra pregunta en una segunda continuación, esta sobre los TVP:

  • STRING_SPLIT() en SQL Server 2016:seguimiento n.º 2