¿Ayuda dividir la consulta SQL con muchas combinaciones en otras más pequeñas?


18

Necesitamos hacer algunos informes todas las noches en nuestro SQL Server 2008 R2. Calcular los informes lleva varias horas. Para acortar el tiempo, precalculamos una tabla. Esta tabla se creó en base a UNIRSE a 12 tablas bastante grandes (decenas de millones de filas).

El cálculo de esta tabla de agregación tomó hasta hace unos días cca 4 horas. Nuestro DBA que dividió esta gran unión en 3 uniones más pequeñas (cada una uniendo 4 tablas). El resultado temporal se guarda en una tabla temporal cada vez, que se usa en la próxima unión.

El resultado de la mejora de DBA es que la tabla de agregación se calcula en 15 minutos. Me preguntaba cómo es eso posible. DBA me dijo que es porque la cantidad de datos que el servidor debe procesar es menor. En otras palabras, que en la gran unión original, el servidor tiene que trabajar con más datos que en uniones más pequeñas sumadas. Sin embargo, supongo que el optimizador se encargaría de hacerlo de manera eficiente con la unión grande original, dividiendo las uniones por sí solo y enviando solo el número de columnas necesarias para las próximas uniones.

La otra cosa que ha hecho es que creó un índice en una de las tablas temporales. Sin embargo, una vez más, pensaría que el optimizador creará las tablas hash apropiadas si es necesario y optimizará mejor el cálculo.

Hablé sobre esto con nuestro DBA, pero él mismo no estaba seguro de qué causó la mejora en el tiempo de procesamiento. Él acaba de mencionar que no culparía al servidor, ya que puede ser abrumador calcular datos tan grandes y que es posible que el optimizador tenga dificultades para predecir el mejor plan de ejecución ... Entiendo esto, pero me gustaría tener una respuesta más definitoria en cuanto a exactamente por qué.

Entonces, las preguntas son:

  1. ¿Qué podría causar la gran mejora?

  2. ¿Es un procedimiento estándar dividir uniones grandes en pequeñas?

  3. ¿La cantidad de datos que el servidor tiene que procesar es realmente menor en caso de múltiples uniones más pequeñas?

Aquí está la consulta original:

    Insert Into FinalResult_Base
SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TSK.CategoryId
    ,TT.[TestletId]
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) 
    ,TQ.[QuestionId]
    ,TS.StudentId
    ,TS.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] 
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,TS.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,TQ.[Position]  
    ,RA.SpecialNeeds        
    ,[Version] = 1 
    ,TestAdaptationId = TA.Id
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,AnswerType = TT.TestletAnswerTypeId
FROM 
    [TestQuestion] TQ WITH (NOLOCK)
    Join [TestTask] TT WITH (NOLOCK)            On TT.Guid = TQ.TestTaskId
    Join [Question] Q WITH (NOLOCK)         On TQ.QuestionId =  Q.QuestionId
    Join [Testlet] TL WITH (NOLOCK)         On TT.TestletId  = TL.Guid 
    Join [Test]     T WITH (NOLOCK)         On TL.TestId     =  T.Guid
    Join [TestSet] TS WITH (NOLOCK)         On T.TestSetId   = TS.Guid 
    Join [RoleAssignment] RA WITH (NOLOCK)  On TS.StudentId  = RA.PersonId And RA.RoleId = 1
    Join [Task] TSK WITH (NOLOCK)       On TSK.TaskId = TT.TaskId
    Join [Category] C WITH (NOLOCK)     On C.CategoryId = TSK.CategoryId
    Join [TimeWindow] TW WITH (NOLOCK)      On TW.Id = TS.TimeWindowId 
    Join [TestAdaptation] TA WITH (NOLOCK)  On TA.Id = TW.TestAdaptationId
    Join [TestCampaign] TC WITH (NOLOCK)        On TC.TestCampaignId = TA.TestCampaignId 
WHERE
    T.TestTypeId = 1    -- eliminuji ankety 
    And t.ProcessedOn is not null -- ne vsechny, jen dokoncene
    And TL.ShownOn is not null
    And TS.Redizo not in (999999999, 111111119)
END;

La nueva división se une después del gran trabajo de DBA:

    SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) -- prevod na A5, B4, B5 ...
    ,TS.StudentId
    ,TS.ClassId
    ,TS.Redizo
    ,[Version] = 1 -- ? 
    ,TestAdaptationId = TA.Id
    ,TL.Guid AS TLGuid
    ,TS.TimeWindowId
INTO
    [#FinalResult_Base_1]
FROM 
    [TestSet] [TS] WITH (NOLOCK)
    JOIN [Test] [T] WITH (NOLOCK) 
        ON [T].[TestSetId] = [TS].[Guid] AND [TS].[Redizo] NOT IN (999999999, 111111119) AND [T].[TestTypeId] = 1 AND [T].[ProcessedOn] IS NOT NULL
    JOIN [Testlet] [TL] WITH (NOLOCK)
        ON [TL].[TestId] = [T].[Guid] AND [TL].[ShownOn] IS NOT NULL
    JOIN [TimeWindow] [TW] WITH (NOLOCK)
        ON [TW].[Id] = [TS].[TimeWindowId] AND [TW].[IsActive] = 1
    JOIN [TestAdaptation] [TA] WITH (NOLOCK)
        ON [TA].[Id] = [TW].[TestAdaptationId] AND [TA].[IsActive] = 1
    JOIN [TestCampaign] [TC] WITH (NOLOCK)
        ON [TC].[TestCampaignId] = [TA].[TestCampaignId] AND [TC].[IsActive] = 1
    JOIN [TestCampaignContainer] [TCC] WITH (NOLOCK)
        ON [TCC].[TestCampaignContainerId] = [TC].[TestCampaignContainerId] AND [TCC].[IsActive] = 1
    ;

 SELECT       
    FR1.TestCampaignContainerId,
    FR1.TestCampaignCategoryId,
    FR1.Grade,
    FR1.TestCampaignId,    
    FR1.TestSetId
    ,FR1.TestId
    ,TSK.CategoryId AS [TaskCategoryId]
    ,TT.[TestletId]
    ,FR1.SectionNo
    ,FR1.Difficulty
    ,TestletName = Char(65+FR1.SectionNo) + CONVERT(varchar(4),6 - FR1.Difficulty) -- prevod na A5, B4, B5 ...
    ,FR1.StudentId
    ,FR1.ClassId
    ,FR1.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,[Version] = 1 -- ? 
    ,FR1.TestAdaptationId
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,AnswerType = TT.TestletAnswerTypeId
    ,TT.Guid AS TTGuid

INTO
    [#FinalResult_Base_2]
FROM 
    #FinalResult_Base_1 FR1
    JOIN [TestTask] [TT] WITH (NOLOCK)
        ON [TT].[TestletId] = [FR1].[TLGuid] 
    JOIN [Task] [TSK] WITH (NOLOCK)
        ON [TSK].[TaskId] = [TT].[TaskId] AND [TSK].[IsActive] = 1
    JOIN [Category] [C] WITH (NOLOCK)
        ON [C].[CategoryId] = [TSK].[CategoryId]AND [C].[IsActive] = 1
    ;    

DROP TABLE [#FinalResult_Base_1]

CREATE NONCLUSTERED INDEX [#IX_FR_Student_Class]
ON [dbo].[#FinalResult_Base_2] ([StudentId],[ClassId])
INCLUDE ([TTGuid])

SELECT       
    FR2.TestCampaignContainerId,
    FR2.TestCampaignCategoryId,
    FR2.Grade,
    FR2.TestCampaignId,    
    FR2.TestSetId
    ,FR2.TestId
    ,FR2.[TaskCategoryId]
    ,FR2.[TestletId]
    ,FR2.SectionNo
    ,FR2.Difficulty
    ,FR2.TestletName
    ,TQ.[QuestionId]
    ,FR2.StudentId
    ,FR2.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] -- 1+ good, 0 wrong, null no answer
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 -- cookie
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,FR2.Redizo
    ,FR2.ViewCount
    ,FR2.SpentTime
    ,TQ.[Position] AS [QuestionPosition]  
    ,RA.SpecialNeeds -- identifikace SVP        
    ,[Version] = 1 -- ? 
    ,FR2.TestAdaptationId
    ,FR2.TaskId
    ,FR2.TaskPosition
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,FR2.AnswerType
INTO
    [#FinalResult_Base]
FROM 
    [#FinalResult_Base_2] FR2
    JOIN [TestQuestion] [TQ] WITH (NOLOCK)
        ON [TQ].[TestTaskId] = [FR2].[TTGuid]
    JOIN [Question] [Q] WITH (NOLOCK)
        ON [Q].[QuestionId] = [TQ].[QuestionId] AND [Q].[IsActive] = 1

    JOIN [RoleAssignment] [RA] WITH (NOLOCK)
        ON [RA].[PersonId] = [FR2].[StudentId]
        AND [RA].[ClassId] = [FR2].[ClassId] AND [RA].[IsActive] = 1 AND [RA].[RoleId] = 1

    drop table #FinalResult_Base_2;

    truncate table [dbo].[FinalResult_Base];
    insert into [dbo].[FinalResult_Base] select * from #FinalResult_Base;

    drop table #FinalResult_Base;

3
Una palabra de advertencia: WITH (NOLOCK) es malo, puede provocar que vuelvan datos incorrectos. Sugiero intentar CON (ROWCOMMITTED).
TomTom

1
@TomTom ¿Querías decir READCOMMITTED? Nunca he visto ROWCOMMITTED antes.
ypercubeᵀᴹ

44
CON (NOLOCK) no es malo. Simplemente no es la bala mágica que la gente parece pensar que es. Como la mayoría de las cosas en SQL Server y el desarrollo de software en general, tiene su lugar.
Zane

2
Sí, pero dado que NOLOCK puede generar advertencias en el registro y, lo que es más importante, devolver DATOS INCORRECTOS, lo considero malo. Solo se puede usar en tablas GARANTIZADAS para no cambiar la clave principal y las claves seleccionadas mientras se ejecuta la consulta. Y sí, yo y READCOMMITED, lo siento.
TomTom

Respuestas:


11

1 Reducción del 'espacio de búsqueda', junto con mejores estadísticas para las uniones intermedias / tardías.

Tuve que lidiar con combinaciones de 90 tablas (diseño de mickey mouse) donde el Procesador de consultas se negó incluso a crear un plan. Romper dicha unión en 10 subunidades de 9 tablas cada una, redujo drásticamente la complejidad de cada combinación, que crece exponencialmente con cada tabla adicional. Además, el Optimizador de consultas ahora los trata como 10 planes, gastando (potencialmente) más tiempo en general (¡Paul White incluso puede tener métricas!).

Las tablas de resultados intermedios ahora tendrán estadísticas nuevas propias, uniéndose mucho mejor en comparación con las estadísticas de un árbol profundo que se sesga desde el principio y termina como Ciencia Ficción poco después.

Además, puede forzar primero las uniones más selectivas, reduciendo los volúmenes de datos que se mueven hacia arriba en el árbol. Si puede estimar la selectividad de sus predicados mucho mejor que el Optimizador, ¿por qué no forzar el orden de unión? Puede valer la pena buscar "Planes espesos".

2 Se debe considerarse, en mi opinión, si la eficiencia y el rendimiento son importantes

3 No necesariamente, pero podría ser si las uniones más selectivas se ejecutan desde el principio


3
+1 gracias. Especialmente para la descripción de tu experiencia. Muy cierto al decir esto "Si puede estimar la selectividad de sus predicados mucho mejor que el Optimizador, ¿por qué no forzar el orden de unión".
Ondrej Peterka

2
Es una pregunta muy válida en realidad. La unión de 90 mesas podría ser forzada a producir un plan simplemente usando la opción 'Forzar orden'. No importó que el orden fuera probablemente aleatorio y subóptimo, solo reducir el espacio de búsqueda fue suficiente para ayudar al Optimizador a crear un plan en un par de segundos (sin la sugerencia de que se agotaría después de 20 segundos).
John Alan

6
  1. El optimizador de SQLServer generalmente hace un buen trabajo. Sin embargo, su objetivo no es generar el mejor plan posible, sino encontrar el plan lo suficientemente rápido. Para una consulta particular con muchas combinaciones, puede causar un rendimiento muy bajo. Una buena indicación de tal caso es una gran diferencia entre el número estimado y real de filas en el plan de ejecución real. Además, estoy bastante seguro de que el plan de ejecución para la consulta inicial mostrará muchas 'uniones de bucles anidados' que son más lentas que 'unir fusión'. Este último requiere que ambas entradas se clasifiquen utilizando la misma clave, lo cual es costoso, y generalmente el optimizador descarta dicha opción. Almacenar resultados en una tabla temporal y agregar índices adecuados como lo hizo, supongo, al elegir un mejor algoritmo para nuevas uniones (nota al margen: primero se siguen las mejores prácticas al completar la tabla temporal, y agregando índices después). Además, SQLServer genera y mantiene estadísticas para tablas temporales que también ayudan a elegir el índice adecuado.
  2. No puedo decir que haya un estándar sobre el uso de tablas temporales cuando el número de uniones es mayor que un número fijo, pero definitivamente es una opción que puede mejorar el rendimiento. Eso no sucede a menudo, pero tuve problemas similares (y una solución similar) un par de veces. Alternativamente, puede intentar averiguar el mejor plan de ejecución usted mismo, almacenarlo y forzarlo a volver a usarlo, pero tomará una cantidad enorme de tiempo (no 100% garantizado de que tenga éxito). Otra nota al margen: en caso de que el conjunto de resultados que se almacena en la tabla temporal sea relativamente pequeño (digamos unos 10k registros), la variable de tabla funciona mejor que la tabla temporal.
  3. Odio decir 'eso depende', pero probablemente sea mi respuesta a su tercera pregunta. El optimizador tiene que dar resultados rápidamente; no quiere que pase horas tratando de encontrar el mejor plan; cada combinación agrega trabajo adicional y, a veces, el optimizador "se confunde".

3
+1 gracias por la confirmación y explicación. Lo que has escrito tiene sentido.
Ondrej Peterka

4

Bueno, permítanme comenzar diciendo que trabajan con datos pequeños: 10 millones de millones no son grandes. El último proyecto DWH que había agregado 400 millones de filas a la tabla de hechos. POR DÍA. Almacenaje por 5 años.

El problema es el hardware, parcialmente. Como las grandes uniones pueden usar MUCHO espacio temporal y solo hay tanta RAM, en el momento en que se desborda en el disco, las cosas se vuelven mucho más lentas. Como tal, puede tener sentido dividir el trabajo en partes más pequeñas simplemente porque si bien SQL vive en un mundo de conjuntos y no le importa el tamaño, el servidor en el que se ejecuta no es infinito. Estoy bastante acostumbrado a salir de los errores de espacio en un tempdb de 64 gb durante algunas operaciones.

De lo contrario, siempre que las estadísticas estén en orden, el optimizador de consultas no se verá abrumado. Realmente no le importa cuán grande sea la tabla: funciona con estadísticas que realmente no crecen. ESO DIJO: Si realmente tiene una tabla GRANDE (número de mil millones de filas de dos dígitos), entonces pueden ser un poco gruesas.

También hay una cuestión de bloqueo, a menos que programe bien que la unión grande puede bloquear la mesa durante horas. Estoy haciendo operaciones de copia de 200 gb en este momento, y las estoy dividiendo en smllerparty por una clave comercial (efectivamente en bucle) que mantiene los bloqueos mucho más cortos.

Al final, trabajamos con hardware limitado.


1
+1 gracias por tu respuesta. Es bueno decir que depende de HW. Tenemos solo 32 GB de RAM, lo que probablemente no sea suficiente.
Ondrej Peterka

2
Me siento un poco frustrado cada vez que leo respuestas como esa, incluso unas pocas docenas de millones de filas crean carga de CPU en nuestro servidor de base de datos durante horas. Tal vez el número de dimensiones es alto, pero 30 dimensiones no parecen ser un número demasiado grande. Creo que la gran cantidad de filas que puede procesar proviene de un modelo simple. Peor aún: toda la información cabe en la RAM. Y todavía lleva horas.
flaschenpost

1
30 dimensiones es MUCHO - ¿estás seguro de que el modelo está correctamente optimizado para convertirse en una estrella? Algunos errores, por ejemplo, que cuestan CPU: en la consulta OP se utilizan GUID como claves principales (identificador único). También me encantan: como índice único, la clave principal es un campo ID, hace que toda la comparación sea más rápida y el índice sea más nawwox (4 u 8 bytes, no 18). Trucos como ese ahorran una TONELADA de CPU.
TomTom
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.