¿Cuál es la forma más eficiente de obtener el mínimo de múltiples columnas en SQL Server 2005?

29

Estoy en una situación en la que quiero obtener el valor mínimo de 6 columnas.

Hasta ahora he encontrado tres formas de lograr esto, pero me preocupa el rendimiento de estos métodos y me gustaría saber cuál sería mejor para el rendimiento.

El primer método es usar una declaración de caso grande . Aquí hay un ejemplo con 3 columnas, basado en el ejemplo en el enlace de arriba. Mi declaración de caso sería mucho más larga ya que miraré 6 columnas.

Select Id,
       Case When Col1 <= Col2 And Col1 <= Col3 Then Col1
            When Col2 <= Col3 Then Col2 
            Else Col3
            End As TheMin
From   MyTable

La segunda opción es usar el UNIONoperador con múltiples sentencias select . Pondría esto en un UDF que acepte un parámetro Id.

select Id, dbo.GetMinimumFromMyTable(Id)
from MyTable

y

select min(col)
from
(
    select col1 [col] from MyTable where Id = @id
    union all
    select col2 from MyTable where Id = @id
    union all
    select col3 from MyTable where Id = @id
) as t

Y la tercera opción que encontré fue usar el operador UNPIVOT , que ni siquiera sabía que existía hasta ahora

with cte (ID, Col1, Col2, Col3)
as
(
    select ID, Col1, Col2, Col3
    from TestTable
)
select cte.ID, Col1, Col2, Col3, TheMin from cte
join
(
    select
        ID, min(Amount) as TheMin
    from 
        cte 
        UNPIVOT (Amount for AmountCol in (Col1, Col2, Col3)) as unpvt
    group by ID
) as minValues
on cte.ID = minValues.ID

Debido al tamaño de la tabla y la frecuencia con la que se consulta y actualiza esta tabla, me preocupa el impacto en el rendimiento que estas consultas tendrían en la base de datos.

Esta consulta se usará realmente en una unión a una tabla con unos pocos millones de registros, sin embargo, los registros devueltos se reducirán a alrededor de cien registros a la vez. Se ejecutará muchas veces durante el día, y las 6 columnas que estoy consultando se actualizan con frecuencia (contienen estadísticas diarias). No creo que haya ningún índice en las 6 columnas que estoy consultando.

¿Cuál de estos métodos es mejor para el rendimiento cuando se intenta obtener el mínimo de varias columnas? ¿O hay otro método mejor que no conozco?

Estoy usando SQL Server 2005

Datos de muestra y resultados

Si mis datos contienen registros como este:

Id Col1 Col2 Col3 Col4 Col5 Col6
1 3 4 0 2 1 5
2 2 6 10 5 7 9
3 1 1 2 3 4 5
4 9 5 4 6 8 9

El resultado final debe ser

Id Value
1 0
2 2
3 1
4 4
Rachel
fuente

Respuestas:

22

Probé el rendimiento de los 3 métodos, y esto es lo que encontré:

  • 1 registro: sin diferencia notable
  • 10 registros: sin diferencia notable
  • 1,000 registros: sin diferencia notable
  • 10,000 registros: la UNIONsubconsulta fue un poco más lenta. La CASE WHENconsulta es un poco más rápida que la UNPIVOT.
  • 100,000 registros: la UNIONsubconsulta es significativamente más lenta, pero la UNPIVOTconsulta se vuelve un poco más rápida que la CASE WHENconsulta
  • 500,000 registros: la UNIONsubconsulta sigue siendo significativamente más lenta, pero se UNPIVOTvuelve mucho más rápida que la CASE WHENconsulta

Entonces los resultados finales parecen ser

  • Con conjuntos de registros más pequeños, no parece haber suficiente diferencia para importar. Use lo que sea más fácil de leer y mantener.

  • Una vez que comience a ingresar en conjuntos de registros más grandes, la UNION ALLsubconsulta comienza a funcionar mal en comparación con los otros dos métodos.

  • La CASEdeclaración realiza el mejor hasta cierto punto (en mi caso, alrededor de 100k filas), y en qué punto la UNPIVOTconsulta se convierte en la consulta de mejor rendimiento

El número real en el que una consulta se vuelve mejor que otra probablemente cambiará como resultado de su hardware, esquema de base de datos, datos y carga actual del servidor, así que asegúrese de probar con su propio sistema si le preocupa el rendimiento.

También realicé algunas pruebas con la respuesta de Mikael ; sin embargo, fue más lento que los otros 3 métodos probados aquí para la mayoría de los tamaños de conjuntos de registros. La única excepción fue que funcionó mejor que una UNION ALLconsulta para tamaños de conjuntos de registros muy grandes. Sin embargo, me gusta el hecho de que muestra el nombre de la columna además del valor más pequeño.

No soy un dba, por lo que es posible que no haya optimizado mis pruebas y me haya perdido algo. Estaba probando con los datos reales en vivo, por lo que eso puede haber afectado los resultados. Traté de dar cuenta de eso ejecutando cada consulta unas pocas veces diferentes, pero nunca se sabe. Definitivamente me interesaría si alguien escribiera una prueba limpia de esto y compartiera sus resultados.

Rachel
fuente
6

No sé qué es lo más rápido, pero podrías probar algo como esto.

declare @T table
(
  Col1 int,
  Col2 int,
  Col3 int,
  Col4 int,
  Col5 int,
  Col6 int
)

insert into @T values(1, 2, 3, 4, 5, 6)
insert into @T values(2, 3, 1, 4, 5, 6)

select T4.ColName, T4.ColValue
from @T as T1
  cross apply (
                select T3.ColValue, T3.ColName
                from (
                       select row_number() over(order by T2.ColValue) as rn,
                              T2.ColValue,
                              T2.ColName
                       from (
                              select T1.Col1, 'Col1' union all
                              select T1.Col2, 'Col2' union all
                              select T1.Col3, 'Col3' union all
                              select T1.Col4, 'Col4' union all
                              select T1.Col5, 'Col5' union all
                              select T1.Col6, 'Col6'
                            ) as T2(ColValue, ColName)
                     ) as T3
                where T3.rn = 1
              ) as T4

Resultado:

ColName ColValue
------- -----------
Col1    1
Col3    1

Si no está interesado en qué columna tiene el valor mínimo, puede usar esto en su lugar.

declare @T table
(
  Id int,
  Col1 int,
  Col2 int,
  Col3 int,
  Col4 int,
  Col5 int,
  Col6 int
)

insert into @T
select 1,        3,       4,       0,       2,       1,       5 union all
select 2,        2,       6,      10,       5,       7,       9 union all
select 3,        1,       1,       2,       3,       4,       5 union all
select 4,        9,       5,       4,       6,       8,       9

select T.Id, (select min(T1.ColValue)
              from (
                      select T.Col1 union all
                      select T.Col2 union all
                      select T.Col3 union all
                      select T.Col4 union all
                      select T.Col5 union all
                      select T.Col6
                    ) as T1(ColValue)
             ) as ColValue
from @T as T

Una consulta simplificada sin pivote.

select Id, min(ColValue) as ColValue
from @T
unpivot (ColValue for Col in (Col1, Col2, Col3, Col4, Col5, Col6)) as U
group by Id
Mikael Eriksson
fuente
6

Agregue una columna calculada persistente que use una CASEinstrucción para hacer la lógica que necesita.

El valor mínimo siempre estará disponible de manera eficiente cuando necesite hacer una unión (o cualquier otra cosa) en función de ese valor.

El valor se volverá a calcular cada vez que cambie cualquiera de los valores de origen ( INSERT/ UPDATE/ MERGE). No digo que esta sea necesariamente la mejor solución para la carga de trabajo, simplemente la ofrezco como una solución, al igual que las otras respuestas. Solo el OP puede determinar cuál es el mejor para la carga de trabajo.

Jon Seigel
fuente
1

Declaración de caso para 6 fechas. Para hacer menos, copie la rama verdadera de la primera declaración del caso. El peor caso es cuando Date1 es el valor más bajo, el mejor caso es cuando Date6 es el valor más bajo, por lo tanto, coloque la fecha más probable en Date6. Escribí esto debido a las limitaciones de las columnas calculadas.

CASE WHEN Date1 IS NULL OR Date1 > Date2 THEN
        CASE WHEN Date2 IS NULL OR Date2 > Date3 THEN
            CASE WHEN Date3 IS NULL OR Date3 > Date4 THEN
                CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                        Date6
                    ELSE
                        Date4
                    END
                END
            ELSE
                CASE WHEN Date3 IS NULL OR Date3 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date3 IS NULL OR Date3 > Date6 THEN
                        Date6
                    ELSE
                        Date3
                    END
                END
            END
        ELSE
            CASE WHEN Date2 IS NULL OR Date2 > Date4 THEN
                CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                        CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                            Date6
                        ELSE
                            Date5
                        END
                    ELSE
                        CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                            Date6
                        ELSE
                            Date4
                        END
                    END
                END
            ELSE
                CASE WHEN Date2 IS NULL OR Date2 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date2 IS NULL OR Date2 > Date6 THEN
                        Date6
                    ELSE
                        Date2
                    END
                END
            END
        END
ELSE
    CASE WHEN Date1 IS NULL OR Date1 > Date3 THEN
        CASE WHEN Date3 IS NULL OR Date3 > Date4 THEN
            CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                    Date6
                ELSE
                    Date4
                END
            END
        ELSE
            CASE WHEN Date3 IS NULL OR Date3 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date3 IS NULL OR Date3 > Date6 THEN
                    Date6
                ELSE
                    Date3
                END
            END
        END
    ELSE
        CASE WHEN Date1 IS NULL OR Date1 > Date4 THEN
            CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                    Date6
                ELSE
                    Date4
                END
            END
        ELSE
            CASE WHEN Date1 IS NULL OR Date1 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date1 IS NULL OR Date1 > Date6 THEN
                    Date6
                ELSE
                    Date1
                END
            END
        END
    END
END

Si se encontró con esta página simplemente buscando comparar fechas y no está tan preocupado por el rendimiento o la compatibilidad, puede usar un Constructor de valores de tabla, que se puede usar donde se permitan las subselecciones (SQL Server 2008 y versiones posteriores):

Lowest =    
(
    SELECT MIN(TVC.d) 
    FROM 
    (
        VALUES
            (Date1), 
            (Date2), 
            (Date3), 
            (Date4), 
            (Date5), 
            (Date6)
    ) 
    AS TVC(d)
)
Jesse Adam
fuente
1

Su casedeclaración no es eficiente. Estás haciendo 5 comparaciones en el peor de los casos, y 2 en el mejor de los casos; mientras que encontrar el mínimo de ndebería hacer en la mayoría de las n-1comparaciones

Para cada fila, en promedio, está haciendo 3.5 comparaciones en lugar de 2. Por lo tanto, lleva más tiempo de CPU y es lento. Intente sus pruebas nuevamente usando la siguiente casedeclaración. Solo está usando 2 comparaciones por fila y debería ser más eficiente que unpivoty union all.

Select Id, 
       Case 
           When Col1 <= Col2 then case when Col1 <= Col3 Then Col1  else col3 end
            When  Col2 <= Col3 Then Col2  
            Else Col3 
            End As TheMin 
From   YourTableNameHere

El union allmétodo es incorrecto en su caso, ya que obtiene el valor mínimo no por fila, sino para toda la tabla. Además, no será eficiente ya que va a escanear la misma tabla 3 veces. Cuando la tabla es pequeña, la E / S no hará mucha diferencia, pero para tablas grandes sí lo hará. No uses ese método.

Unpivotes bueno e intente la desconexión manual también mediante el uso de la combinación cruzada con su tabla (select 1 union all select 2 union all select 3). Debería ser tan eficiente como el unpivot.

La mejor solución sería tener una columna persistente calculada, si no tiene problemas de espacio. Se agregará al tamaño de la fila en 4 bytes (supongo que tendrá inttipo), lo que a su vez aumentará el tamaño de la tabla.

Sin embargo, el espacio y la memoria son un problema en su sistema y la CPU no lo hace, entonces no lo haga persistente, sino que use una columna calculada simple usando la declaración de caso. Hará que el código sea más simple.

Gulli Meel
fuente
-1

Supongo que la primera opción es la más rápida (¡aunque no parece muy elegante desde la perspectiva de la programación!). Esto se debe a que trata exactamente N filas (donde N es el tamaño de la tabla) y no tiene que buscar ni ordenar como el método 2 o 3.

Una prueba con una muestra grande debería probar el punto.

Como otra opción a tener en cuenta (¡como si necesitara más!), Es crear una vista materializada sobre su tabla. si el tamaño de su mesa es de cientos de miles o más. De esta forma, el valor mínimo se calcula mientras se cambia la fila y no se tendría que procesar toda la tabla con cada consulta. En SQL Server, las vistas materializadas se denominan Vistas indexadas

Ninguna posibilidad
fuente
-1
Create table #temp
   (
    id int identity(1,1),
    Name varchar(30),
    Year1 int,
    Year2 int,
    Year3 int,
    Year4 int
   )

   Insert into #temp values ('A' ,2015,2016,2014,2010)
   Insert into #temp values ('B' ,2016,2013,2017,2018)
   Insert into #temp values ('C' ,2010,2016,2014,2017)
   Insert into #temp values ('D' ,2017,2016,2014,2015)
   Insert into #temp values ('E' ,2016,2016,2016,2016)
   Insert into #temp values ('F' ,2016,2017,2018,2019)
   Insert into #temp values ('G' ,2016,2017,2020,2019)

   Select *, Case 
                 when Year1 >= Year2 and Year1 >= Year3 and Year1 >= Year4 then Year1
                 when Year2 >= Year3 and Year2 >= Year4 and Year2 >= Year1 then Year2
                 when Year3 >= Year4 and Year3 >= Year1 and Year3 >= Year2 then Year3
                 when Year4 >= Year1 and Year4 >= Year2 and Year4 >= Year3 then Year4  
                 else Year1 end as maxscore  
                 from #temp
Ravi
fuente
No está contabilizando NULL, eso hace que su expresión CASE sea relativamente simple. Sin embargo, si al menos una de las columnas es NULL, su solución regresará Year1como resultado, lo que puede no ser necesariamente correcto.
Andriy M