La inclusión de ORDER BY en la consulta que no devuelve filas afecta drásticamente el rendimiento

15

Dada una combinación simple de tres tablas, el rendimiento de la consulta cambia drásticamente cuando ORDER BY se incluye incluso sin que se devuelvan filas. El escenario del problema real tarda 30 segundos en devolver filas cero, pero es instantáneo cuando ORDER BY no está incluido. ¿Por qué?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Entiendo que podría tener un índice en bigtable.smallGuidId, pero creo que en realidad lo empeoraría en este caso.

Aquí está la secuencia de comandos para crear / llenar las tablas para la prueba. Curiosamente, parece importar que smalltable tenga un campo nvarchar (max). También parece importar que me una a la tabla grande con un guid (lo que supongo que hace que quiera usar la coincidencia de hash).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

He probado en SQL 2005, 2008 y 2008R2 con los mismos resultados.

Hafthor
fuente

Respuestas:

32

Estoy de acuerdo con la respuesta de Martin Smith, pero el problema no es simplemente una estadística, exactamente. Las estadísticas para la columna foreignId (suponiendo que las estadísticas automáticas estén habilitadas) muestran con precisión que no existen filas para un valor de 3 (solo hay una, con un valor de 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

salida de estadísticas

SQL Server sabe que las cosas pueden haber cambiado desde que se capturaron las estadísticas, por lo que puede haber una fila para el valor 3 cuando se ejecuta el plan . Además, puede transcurrir cualquier cantidad de tiempo entre la compilación y la ejecución del plan (los planes se almacenan en caché para su reutilización, después de todo). Como dice Martin, SQL Server contiene lógica para detectar cuándo se han realizado suficientes modificaciones para justificar la recompilación de cualquier plan en caché por razones de optimización.

Sin embargo, nada de esto finalmente importa. Con una excepción de caso límite, el optimizador nunca estimará que el número de filas producidas por una operación de tabla sea cero. Si puede determinar estáticamente que la salida siempre debe ser cero filas, la operación es redundante y se eliminará por completo.

En cambio, el modelo del optimizador estima un mínimo de una fila. Emplear esta heurística tiende a producir mejores planes en promedio de lo que sería el caso si fuera posible una estimación más baja. Un plan que produzca una estimación de fila cero en algún momento sería inútil desde ese punto en adelante en la secuencia de procesamiento, ya que no habría base para tomar decisiones basadas en el costo (las filas cero son filas cero sin importar lo que pase). Si la estimación resulta ser incorrecta, la forma del plan por encima de la estimación de la fila cero casi no tiene posibilidades de ser razonable.

El segundo factor es otro supuesto de modelado llamado Supuesto de contención. Esto esencialmente dice que si una consulta une un rango de valores con otro rango de valores, es porque los rangos se superponen. Otra forma de decir esto es decir que la unión se está especificando porque se espera que se devuelvan las filas. Sin este razonamiento, los costos generalmente se subestimarían, lo que daría como resultado planes deficientes para una amplia gama de consultas comunes.

Esencialmente, lo que tiene aquí es una consulta que no se ajusta al modelo del optimizador. No hay nada que podamos hacer para 'mejorar' las estimaciones con índices de varias columnas o filtrados; No hay forma de obtener una estimación inferior a 1 fila aquí. Una base de datos real puede tener claves externas para garantizar que esta situación no pueda surgir, pero suponiendo que no sea aplicable aquí, nos queda usar sugerencias para corregir la condición fuera del modelo. Cualquier número de enfoques de sugerencias diferentes funcionará con esta consulta. OPTION (FORCE ORDER)es uno que funciona bien con la consulta tal como está escrita.

Paul White reinstala a Monica
fuente
21

El problema básico aquí es uno de las estadísticas.

Para ambas consultas, el recuento de filas estimado muestra que cree que la final SELECTdevolverá 1.048.580 filas (el mismo número de filas que se estima que existen bigtable) en lugar del 0 que sigue.

Ambas JOINcondiciones coinciden y preservarían todas las filas. Terminan siendo eliminados porque la fila individual tinytableno coincide con el t.foreignId=3predicado.

Si tu corres

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

y mire el número estimado de filas en 1lugar de 0y este error se propaga por todo el plan. tinytableActualmente contiene 1 fila. Las estadísticas no se volverían a compilar para esta tabla hasta que se hayan producido 500 modificaciones de fila para que se pueda agregar una fila coincidente y no desencadenaría una recompilación.

La razón por la cual el orden de unión cambia cuando agrega la ORDER BYcláusula y hay una varchar(max)columna smalltablees porque estima que las varchar(max)columnas aumentarán el tamaño de la fila en 4,000 bytes en promedio. Multiplique eso por 1048580 filas y significa que la operación de clasificación necesitaría un estimado de 4GB, por lo que decide sensatamente hacer la SORToperación antes del JOIN.

Puede obligar a la ORDER BYconsulta a adoptar la ORDER BYestrategia de no unión con el uso de sugerencias como se muestra a continuación.

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

El plan muestra un operador de clasificación con un costo 12,000estimado de subárbol de recuentos de filas estimados casi y erróneos y el tamaño de datos estimado.

Plan

Por cierto, no encontré que el reemplazo de las UNIQUEIDENTIFIERcolumnas con números enteros alteró las cosas en mi prueba.

Martin Smith
fuente
2

Active el botón Mostrar plan de ejecución y podrá ver lo que sucede. Aquí está el plan para la consulta "lenta": ingrese la descripción de la imagen aquí

Y aquí está la consulta "rápida": ingrese la descripción de la imagen aquí

Mire eso: ejecute juntos, la primera consulta es ~ 33 veces más "cara" (relación 97: 3). SQL está optimizando la primera consulta para ordenar BigTable por fecha y hora, luego ejecuta un pequeño ciclo de "búsqueda" sobre SmallTable y TinyTable, ejecutándolos 1 millón de veces cada uno (puede pasar el cursor sobre el icono "Búsqueda de índice agrupado" para obtener más estadísticas). Entonces, el tipo (27%) y 2 x 1 millón de "búsquedas" en tablas pequeñas (23% y 46%) son la mayor parte de la consulta costosa. En comparación, la no ORDER BYconsulta realiza un total de 3 escaneos.

Básicamente, ha encontrado un agujero en la lógica del optimizador de SQL para su escenario particular. Pero como lo indica TysHTTP, si agrega un índice (que ralentiza su inserción / actualización), su escaneo se vuelve loco rápidamente.

jklemmack
fuente
2

Lo que sucede es que SQL está decidiendo ejecutar el pedido antes de la restricción.

Prueba esto:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Esto le proporciona un rendimiento mejorado (en este caso, donde el recuento de resultados devuelto es muy pequeño), sin que el rendimiento se vea afectado al agregar otro índice. Si bien es extraño cuando el optimizador de SQL decide realizar el pedido antes de la unión, es probable que si realmente tuviera datos de retorno, ordenarlos después de las uniones llevaría más tiempo que ordenar sin ellas.

Por último, intente ejecutar el siguiente script y luego vea si las estadísticas e índices actualizados solucionan el problema que tiene:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "
Seph
fuente