¿Por qué mi consulta SELECT DISTINCT TOP N escanea toda la tabla?

28

Me he encontrado con algunas SELECT DISTINCT TOP Nconsultas que parecen estar mal optimizadas por el optimizador de consultas de SQL Server. Comencemos considerando un ejemplo trivial: una tabla de un millón de filas con dos valores alternos. Voy a usar el GetNums función para generar los datos:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

Para la siguiente consulta:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server puede encontrar dos valores distintos simplemente escaneando la primera página de datos de la tabla, pero escanea todos los datos en su lugar . ¿Por qué SQL Server simplemente no escanea hasta que encuentra el número solicitado de valores distintos?

Para esta pregunta, utilice los siguientes datos de prueba que contienen 10 millones de filas con 10 valores distintos generados en bloques:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

Las respuestas para una tabla con un índice agrupado también son aceptables:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

La siguiente consulta escanea los 10 millones de filas de la tabla . ¿Cómo puedo obtener algo que no escanee toda la tabla? Estoy usando SQL Server 2016 SP1.

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);
Joe Obbish
fuente

Respuestas:

30

Parece que hay tres reglas de optimizador diferentes que pueden realizar la DISTINCToperación en la consulta anterior. La siguiente consulta arroja un error que sugiere que la lista es exhaustiva:

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

Mensaje 8622, Nivel 16, Estado 1, Línea 1

El procesador de consultas no pudo generar un plan de consulta debido a las sugerencias definidas en esta consulta. Vuelva a enviar la consulta sin especificar ninguna sugerencia y sin usar SET FORCEPLAN.

GbAggToSortimplementa el agregado de grupo por (distinto) como un tipo distinto. Este es un operador de bloqueo que leerá todos los datos de la entrada antes de generar filas. GbAggToStrmimplementa el agregado de grupo por como un agregado de flujo (que también requiere una clasificación de entrada en esta instancia). Este también es un operador de bloqueo. GbAggToHSse implementa como una coincidencia hash, que es lo que vimos en el mal plan de la pregunta, pero se puede implementar como una coincidencia hash (agregado) o una coincidencia hash (flujo distinto).

El operador de coincidencia hash ( flujo distinto ) es una forma de resolver este problema porque no está bloqueando. SQL Server debería poder detener el análisis una vez que encuentre suficientes valores distintos.

El operador lógico Flow Distinct escanea la entrada, eliminando duplicados. Mientras que el operador Distinct consume todas las entradas antes de producir cualquier salida, el operador Flow Distinct devuelve cada fila a medida que se obtiene de la entrada (a menos que esa fila sea un duplicado, en cuyo caso se descarta).

¿Por qué la consulta en la pregunta usa la coincidencia hash (agregado) en lugar de la coincidencia hash (flujo distinto)? A medida que el número de valores distintos cambia en la tabla, esperaría que el costo de la consulta de coincidencia hash (flujo distinto) disminuya porque la estimación del número de filas que necesita escanear a la tabla debería disminuir. Esperaría que el costo del plan de coincidencia de hash (agregado) aumente porque la tabla de hash que necesita construir se hará más grande. Una forma de investigar esto es creando una guía de plan . Si creo dos copias de los datos pero aplico una guía de plan a una de ellas, debería poder comparar la coincidencia hash (agregado) con la coincidencia hash (distinta) lado a lado con los mismos datos. Tenga en cuenta que no puedo hacer esto deshabilitando las reglas del optimizador de consultas porque la misma regla se aplica a ambos planes ( GbAggToHS).

Aquí hay una forma de obtener la guía del plan que busco:

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Obtenga el identificador del plan y úselo para crear una guía de plan:

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

Las guías de plan solo funcionan en el texto de consulta exacto, así que copiemos de nuevo de la guía de plan:

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

Restablecer los datos:

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

Obtenga un plan de consulta para la consulta con la guía del plan aplicada:

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Esto tiene el operador de coincidencia hash (flujo distinto) que queríamos con nuestros datos de prueba. Tenga en cuenta que SQL Server espera leer todas las filas de la tabla y que el costo estimado es exactamente el mismo que para el plan con la coincidencia hash (agregado). Las pruebas que hice sugirieron que los costos para los dos planes son idénticos cuando el objetivo de la fila para el plan es mayor o igual que el número de valores distintos que SQL Server espera de la tabla, que en este caso puede derivarse simplemente de estadística. Desafortunadamente (para nuestra consulta), el optimizador elige la coincidencia hash (agregado) sobre la coincidencia hash (flujo distinto) cuando los costos son los mismos. Así que estamos a 0.0000001 unidades de optimizador mágico lejos del plan que queremos.

Una forma de atacar este problema es disminuyendo el objetivo de la fila. Si el objetivo de la fila desde el punto de vista del optimizador es menor que el recuento distintivo de filas, probablemente obtendremos coincidencia hash (flujo distinto). Esto se puede lograr con la OPTIMIZE FORsugerencia de consulta:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Para esta consulta, el optimizador crea un plan como si la consulta solo necesitara la primera fila pero cuando se ejecuta la consulta recupera las primeras 10 filas. En mi máquina, esta consulta escanea 892800 filas X_10_DISTINCT_HEAPy se completa en 299 ms con 250 ms de tiempo de CPU y 2537 lecturas lógicas.

Tenga en cuenta que esta técnica no funcionará si las estadísticas informan solo un valor distinto, lo que podría suceder para las estadísticas muestreadas con datos asimétricos. Sin embargo, en ese caso, es poco probable que sus datos estén empaquetados lo suficientemente denso como para justificar el uso de técnicas como esta. Es posible que no pierda mucho al escanear todos los datos de la tabla, especialmente si eso se puede hacer en paralelo.

Otra forma de atacar este problema es inflando el número de valores distintos estimados que SQL Server espera obtener de la tabla base. Esto fue más difícil de lo esperado. La aplicación de una función determinista no puede aumentar el recuento distintivo de resultados. Si el optimizador de consultas es consciente de ese hecho matemático (algunas pruebas sugieren que es al menos para nuestros propósitos), la aplicación de funciones deterministas (que incluye todas las funciones de cadena ) no aumentará el número estimado de filas distintas.

Muchas de las funciones no deterministas tampoco funcionaron, incluidas las opciones obvias de NEWID()y RAND(). Sin embargo, LAG()hace el truco para esta consulta. El optimizador de consultas espera 10 millones de valores distintos contra la LAGexpresión, lo que fomentará un plan de coincidencia hash (flujo distinto) :

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

En mi máquina, esta consulta escanea 892800 filas X_10_DISTINCT_HEAPy se completa en 1165 ms con 1109 ms de tiempo de CPU y 2537 lecturas lógicas, por lo que LAG()agrega bastante sobrecarga relativa. @Paul White sugirió probar el procesamiento en modo por lotes para esta consulta. En SQL Server 2016 podemos obtener el procesamiento en modo por lotes incluso con MAXDOP 1. Una forma de obtener el procesamiento en modo por lotes para una tabla de almacén de filas es unirse a un CCI vacío de la siguiente manera:

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

Ese código da como resultado este plan de consulta .

Paul señaló que tenía que cambiar la consulta para usar LAG(..., 1)porque LAG(..., 0)no parece ser elegible para la optimización de Agregado de Windows. Este cambio redujo el tiempo transcurrido a 520 ms y el tiempo de CPU a 454 ms.

Tenga en cuenta que el LAG()enfoque no es el más estable. Si Microsoft cambia la suposición de unicidad contra la función, es posible que ya no funcione. Tiene una estimación diferente con el legado CE. Además, este tipo de optimización contra un montón no es necesariamente una buena idea. Si se reconstruye la tabla, es posible terminar en el peor de los casos en el que casi todas las filas deben leerse de la tabla.

Contra una tabla con una columna única (como el ejemplo de índice agrupado en la pregunta) tenemos mejores opciones. Por ejemplo, podemos engañar al optimizador usando una SUBSTRINGexpresión que siempre devuelve una cadena vacía. SQL Server no cree que SUBSTRINGcambie el número de valores distintos, por lo que si lo aplicamos a una columna única, como PK, el número estimado de filas distintas es de 10 millones. La siguiente consulta obtiene el operador de coincidencia hash (flujo distinto):

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

En mi máquina, esta consulta escanea 900000 filas X_10_DISTINCT_CIy se completa en 333 ms con 297 ms de tiempo de CPU y 3011 lecturas lógicas.

En resumen, el optimizador de consultas parece suponer que todas las filas se leerán de la tabla para SELECT DISTINCT TOP Nconsultas cuando N> = el número de filas distintas estimadas de la tabla. El operador de coincidencia hash (agregado) puede tener el mismo costo que el operador de coincidencia hash (flujo distinto) pero el optimizador siempre elige el operador agregado. Esto puede conducir a lecturas lógicas innecesarias cuando se ubican suficientes valores distintos cerca del inicio del análisis de la tabla. Dos maneras de engañar al optimizador para que use el operador de coincidencia hash (flujo distinto) son bajar el objetivo de la fila usando la OPTIMIZE FORpista o aumentar el número estimado de filas distintas usando LAG()o SUBSTRINGen una columna única.

Joe Obbish
fuente
12

Ya has respondido tus propias preguntas correctamente.

Solo quiero agregar una observación de que la forma más eficiente es escanear toda la tabla, si se puede organizar como un 'montón' de almacén de columnas :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

La consulta simple:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

luego da:

Plan de ejecución

Tabla 'X_10_DISTINCT_HEAP'. Escaneo recuento 1,
 lecturas lógicas 0, lecturas físicas 0, lecturas anticipadas 0, 
 lecturas lógicas lob 66 , lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla 'X_10_DISTINCT_HEAP'. El segmento lee 13, el segmento saltó 0.

 Tiempos de ejecución de SQL Server:
   Tiempo de CPU = 0 ms, tiempo transcurrido = 11 ms.

Hash Match (Flow Distinct) no se puede ejecutar actualmente en modo por lotes. Los métodos que usan esto son mucho más lentos debido a la transición costosa (invisible) del procesamiento por lotes a filas. Por ejemplo:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

Da:

Plan de ejecución distintivo de flujo

Tabla 'X_10_DISTINCT_HEAP'. Escaneo recuento 1,
 lecturas lógicas 0, lecturas físicas 0, lecturas anticipadas 0, 
 lecturas lógicas lob 20 , lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla 'X_10_DISTINCT_HEAP'. El segmento lee 4 , el segmento omite 0.

 Tiempos de ejecución de SQL Server:
   Tiempo de CPU = 640 ms, tiempo transcurrido = 680 ms.

Esto es más lento que cuando la tabla se organiza como un montón de almacén de filas.

Paul White dice GoFundMonica
fuente
5

Aquí hay un intento de emular un escaneo parcial repetido (similar pero no igual que un escaneo omitido) usando un CTE recursivo. El objetivo, ya que no tenemos índice activado (id), es evitar géneros y escaneos múltiples en la tabla.

Hace algunos trucos para evitar algunas restricciones recursivas de CTE:

  • No TOPpermitido en la parte recursiva. Usamos una subconsulta y en su ROW_NUMBER()lugar.
  • No podemos tener múltiples referencias a la parte constante o uso LEFT JOINo uso NOT IN (SELECT id FROM cte)de la parte recursiva. Para omitir, construimos una VARCHARcadena que acumula todos los idvalores, similares a STRING_AGGo a jerarquía ID y luego se compara con LIKE.

Para un montón (suponiendo que se nombre la columna id) test-1 en rextester.com .

Esto, como lo han demostrado las pruebas, no evita múltiples escaneos, pero funciona bien cuando se encuentran diferentes valores en las primeras páginas. Sin embargo, si los valores no se distribuyen de manera uniforme, puede realizar múltiples escaneos en grandes partes de la tabla, lo que, por supuesto, da como resultado un bajo rendimiento.

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

y cuando la tabla está agrupada (CI activadounique_key ), prueba-2 en rextester.com .

Esto usa el índice agrupado ( WHERE x.unique_key > ct.unique_key) para evitar múltiples escaneos:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;
ypercubeᵀᴹ
fuente
Hay un problema de rendimiento bastante sutil con esta solución. Termina haciendo una búsqueda adicional en la tabla después de encontrar el enésimo valor. Entonces, si hay 10 valores distintos para un top 10, buscará un undécimo valor que no está allí. Terminas con una exploración completa adicional y los 10 millones de cálculos ROW_NUMBER () realmente se suman. Tengo una solución aquí que acelera la consulta 20X en mi máquina. ¿Qué piensas? brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish
2

Para completar, otra forma de abordar este problema es usar OUTER APPLY . Podemos agregar un OUTER APPLYoperador para cada valor distinto que necesitamos encontrar. Esto es similar en concepto al enfoque recursivo de ypercube, pero efectivamente tiene la recurrencia escrita a mano. Una ventaja es que podemos usar TOPen las tablas derivadas en lugar de la ROW_NUMBER()solución alternativa. Una gran desventaja es que el texto de la consulta se alarga a medida que Naumenta.

Aquí hay una implementación para la consulta contra el montón:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Aquí está el plan de consulta real para la consulta anterior. En mi máquina, esta consulta se completa en 713 ms con 625 ms de tiempo de CPU y 12605 lecturas lógicas. Obtenemos un nuevo valor distinto cada 100k filas, por lo que esperaría que esta consulta escanee alrededor de 900000 * 10 * 0.5 = 4500000 filas. En teoría, esta consulta debería hacer cinco veces las lecturas lógicas de esta consulta de la otra respuesta:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Esa consulta hizo 2537 lecturas lógicas. 2537 * 5 = 12685 que está bastante cerca de 12605.

Para la tabla con el índice agrupado podemos hacerlo mejor. Esto se debe a que podemos pasar el último valor clave agrupado en la tabla derivada para evitar escanear las mismas filas dos veces. Una implementación:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Aquí está el plan de consulta real para la consulta anterior. En mi máquina, esta consulta se completa en 154 ms con 140 ms de tiempo de CPU y 3203 lecturas lógicas. Esto pareció ejecutarse un poco más rápido que la OPTIMIZE FORconsulta en la tabla de índice agrupado. No esperaba eso, así que intenté medir el rendimiento con más cuidado. Mi metodología consistía en ejecutar cada consulta diez veces sin conjuntos de resultados y observar los números agregados de sys.dm_exec_sessionsy sys.dm_exec_session_wait_stats. La sesión 56 fue la APPLYconsulta y la sesión 63 fue la OPTIMIZE FORconsulta.

Salida de sys.dm_exec_sessions:

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

Parece que hay una clara ventaja en cpu_time y elapsed_time para la APPLYconsulta.

Salida de sys.dm_exec_session_wait_stats:

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

La OPTIMIZE FORconsulta tiene un tipo de espera adicional, RESERVED_MEMORY_ALLOCATION_EXT . No sé exactamente qué significa esto. Puede ser solo una medida de la sobrecarga en el operador de coincidencia hash (flujo distinto). En cualquier caso, quizás no valga la pena preocuparse por una diferencia de 70 ms en el tiempo de CPU.

Joe Obbish
fuente
1

Creo que tiene una respuesta sobre por qué
esta puede ser una forma de abordarlo
. Sé que parece desordenado, pero el plan de ejecución dijo que los dos primeros puestos eran el 84% del costo.

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;
paparazzo
fuente
Este código tardó 5 segundos en mi máquina. Parece que las uniones a la variable de tabla agregan bastante sobrecarga. En la consulta final, la variable de tabla se escaneó 892800 veces. Esa consulta tomó 1359 ms de tiempo de CPU y 1374 ms de tiempo transcurrido. Definitivamente más de lo que esperaba. Agregar una clave principal a la variable de tabla parece ayudar, aunque no estoy seguro de por qué. Puede haber otras posibles optimizaciones.
Joe Obbish