Parece que hay tres reglas de optimizador diferentes que pueden realizar la DISTINCT
operació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.
GbAggToSort
implementa 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. GbAggToStrm
implementa 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. GbAggToHS
se 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 FOR
sugerencia 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_HEAP
y 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 LAG
expresió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_HEAP
y 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 SUBSTRING
expresión que siempre devuelve una cadena vacía. SQL Server no cree que SUBSTRING
cambie 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_CI
y 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 N
consultas 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 FOR
pista o aumentar el número estimado de filas distintas usando LAG()
o SUBSTRING
en una columna única.
Para completar, otra forma de abordar este problema es usar OUTER APPLY . Podemos agregar un
OUTER APPLY
operador 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 usarTOP
en las tablas derivadas en lugar de laROW_NUMBER()
solución alternativa. Una gran desventaja es que el texto de la consulta se alarga a medida queN
aumenta.Aquí hay una implementación para la consulta contra el montón:
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:
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:
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 FOR
consulta 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 desys.dm_exec_sessions
ysys.dm_exec_session_wait_stats
. La sesión 56 fue laAPPLY
consulta y la sesión 63 fue laOPTIMIZE FOR
consulta.Salida de
sys.dm_exec_sessions
:Parece que hay una clara ventaja en cpu_time y elapsed_time para la
APPLY
consulta.Salida de
sys.dm_exec_session_wait_stats
:La
OPTIMIZE FOR
consulta 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.fuente
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.
fuente