Publicaré una respuesta para comenzar. Mi primer pensamiento fue que debería ser posible aprovechar la naturaleza de preservación del orden de una unión de bucle anidado junto con algunas tablas auxiliares que tienen una fila para cada letra. La parte difícil iba a estar en bucle de tal manera que los resultados se ordenaran por longitud, así como para evitar duplicados. Por ejemplo, cuando se une de forma cruzada a un CTE que incluye las 26 letras mayúsculas junto con '', puede terminar generando 'A' + '' + 'A'
y'' + 'A' + 'A'
que, por supuesto, es la misma cadena.
La primera decisión fue dónde almacenar los datos de ayuda. Intenté usar una tabla temporal, pero esto tuvo un impacto sorprendentemente negativo en el rendimiento, a pesar de que los datos caben en una sola página. La tabla temporal contenía los siguientes datos:
SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
En comparación con el uso de un CTE, la consulta tardó 3 veces más con una tabla en clúster y 4 veces más con un montón. No creo que el problema sea que los datos están en el disco. Debe leerse en la memoria como una sola página y procesarse en la memoria para todo el plan. Quizás SQL Server pueda trabajar con datos de un operador de exploración constante de manera más eficiente que con los datos almacenados en páginas de almacén de filas típicas.
Curiosamente, SQL Server elige colocar los resultados ordenados de una tabla tempdb de una sola página con datos ordenados en un carrete de tabla:
SQL Server a menudo coloca resultados para la tabla interna de una unión cruzada en un carrete de tabla, incluso si parece absurdo hacerlo. Creo que el optimizador necesita un poco de trabajo en esta área. Ejecuté la consulta con el NO_PERFORMANCE_SPOOL
para evitar el impacto en el rendimiento.
Un problema con el uso de un CTE para almacenar los datos auxiliares es que no se garantiza que los datos se ordenen. No puedo pensar por qué el optimizador elegiría no ordenarlo y en todas mis pruebas los datos se procesaron en el orden en que escribí el CTE:
Sin embargo, es mejor no correr riesgos, especialmente si hay una manera de hacerlo sin una gran sobrecarga de rendimiento. Es posible ordenar los datos en una tabla derivada agregando un TOP
operador superfluo . Por ejemplo:
(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)
Esa adición a la consulta debería garantizar que los resultados se devolverán en el orden correcto. Esperaba que todo este tipo tuviera un gran impacto negativo en el rendimiento. El optimizador de consultas también esperaba esto en función de los costos estimados:
Sorprendentemente, no pude observar ninguna diferencia estadísticamente significativa en el tiempo de CPU o el tiempo de ejecución con o sin un pedido explícito. En todo caso, la consulta parecía correr más rápido con el ORDER BY
! No tengo explicación para este comportamiento.
La parte difícil del problema fue descubrir cómo insertar caracteres en blanco en los lugares correctos. Como se mencionó anteriormente, un simple CROSS JOIN
daría como resultado datos duplicados. Sabemos que la cadena 100000000 tendrá una longitud de seis caracteres porque:
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000
pero
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000
Por lo tanto, solo necesitamos unirnos a la letra CTE seis veces. Supongamos que nos unimos al CTE seis veces, tomamos una letra de cada CTE y las concatenamos todas juntas. Supongamos que la letra de la izquierda no está en blanco. Si alguna de las letras posteriores está en blanco, significa que la cadena tiene menos de seis caracteres, por lo que es un duplicado. Por lo tanto, podemos evitar duplicados al encontrar el primer carácter que no esté en blanco y requerir que todos los caracteres después de él tampoco estén en blanco. Elegí rastrear esto asignando una FLAG
columna a uno de los CTE y agregando un cheque a la WHERE
cláusula. Esto debería quedar más claro después de mirar la consulta. La consulta final es la siguiente:
WITH FIRST_CHAR (CHR) AS
(
SELECT 'A'
UNION ALL SELECT 'B'
UNION ALL SELECT 'C'
UNION ALL SELECT 'D'
UNION ALL SELECT 'E'
UNION ALL SELECT 'F'
UNION ALL SELECT 'G'
UNION ALL SELECT 'H'
UNION ALL SELECT 'I'
UNION ALL SELECT 'J'
UNION ALL SELECT 'K'
UNION ALL SELECT 'L'
UNION ALL SELECT 'M'
UNION ALL SELECT 'N'
UNION ALL SELECT 'O'
UNION ALL SELECT 'P'
UNION ALL SELECT 'Q'
UNION ALL SELECT 'R'
UNION ALL SELECT 'S'
UNION ALL SELECT 'T'
UNION ALL SELECT 'U'
UNION ALL SELECT 'V'
UNION ALL SELECT 'W'
UNION ALL SELECT 'X'
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
SELECT '', 0 CHR
UNION ALL SELECT 'A', 1
UNION ALL SELECT 'B', 1
UNION ALL SELECT 'C', 1
UNION ALL SELECT 'D', 1
UNION ALL SELECT 'E', 1
UNION ALL SELECT 'F', 1
UNION ALL SELECT 'G', 1
UNION ALL SELECT 'H', 1
UNION ALL SELECT 'I', 1
UNION ALL SELECT 'J', 1
UNION ALL SELECT 'K', 1
UNION ALL SELECT 'L', 1
UNION ALL SELECT 'M', 1
UNION ALL SELECT 'N', 1
UNION ALL SELECT 'O', 1
UNION ALL SELECT 'P', 1
UNION ALL SELECT 'Q', 1
UNION ALL SELECT 'R', 1
UNION ALL SELECT 'S', 1
UNION ALL SELECT 'T', 1
UNION ALL SELECT 'U', 1
UNION ALL SELECT 'V', 1
UNION ALL SELECT 'W', 1
UNION ALL SELECT 'X', 1
UNION ALL SELECT 'Y', 1
UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
CASE
WHEN d6.FLAG = 1 THEN 5
WHEN d5.FLAG = 1 THEN 4
WHEN d4.FLAG = 1 THEN 3
WHEN d3.FLAG = 1 THEN 2
WHEN d2.FLAG = 1 THEN 1
ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);
Los CTE son como se describieron anteriormente. ALL_CHAR
se une a cinco veces porque incluye una fila para un carácter en blanco. El carácter final de la cadena no debe estar en blanco de manera separada un CTE se define por ella, FIRST_CHAR
. La columna de bandera adicional en ALL_CHAR
se usa para evitar duplicados como se describió anteriormente. Puede haber una forma más eficiente de hacer esta verificación, pero definitivamente hay formas más ineficientes de hacerlo. Un intento por mi parte LEN()
e POWER()
hizo que la consulta se ejecutara seis veces más lento que la versión actual.
Las sugerencias MAXDOP 1
y FORCE ORDER
son esenciales para asegurarse de que el orden se conserva en la consulta. Un plan estimado anotado podría ser útil para ver por qué las uniones están en su orden actual:
Los planes de consulta a menudo se leen de derecha a izquierda, pero las solicitudes de fila ocurren de izquierda a derecha. Idealmente, SQL Server solicitará exactamente 100 millones de filas del d1
operador de exploración constante. A medida que se mueve de izquierda a derecha, espero que se soliciten menos filas de cada operador. Podemos ver esto en el plan de ejecución real . Además, a continuación se muestra una captura de pantalla de SQL Sentry Plan Explorer:
Obtuvimos exactamente 100 millones de filas de d1, lo cual es algo bueno. Tenga en cuenta que la proporción de filas entre d2 y d3 es casi exactamente 27: 1 (165336 * 27 = 4464072), lo que tiene sentido si piensa en cómo funcionará la unión cruzada. La relación de filas entre d1 y d2 es 22.4, lo que representa algo de trabajo desperdiciado. Creo que las filas adicionales provienen de duplicados (debido a los caracteres en blanco en el medio de las cadenas) que no pasan del operador de unión de bucle anidado que hace el filtrado.
La LOOP JOIN
sugerencia es técnicamente innecesaria porque a CROSS JOIN
solo se puede implementar como una unión de bucle en SQL Server. El NO_PERFORMANCE_SPOOL
es para evitar el carrete innecesario de la mesa. Omitir la sugerencia del carrete hizo que la consulta tardara 3 veces más en mi máquina.
La consulta final tiene un tiempo de CPU de alrededor de 17 segundos y un tiempo total transcurrido de 18 segundos. Eso fue al ejecutar la consulta a través de SSMS y descartar el conjunto de resultados. Estoy muy interesado en ver otros métodos para generar los datos.