¿Cuál es la mejor manera de seleccionar el valor mínimo de varias columnas?


82

Dada la siguiente tabla en SQL Server 2005:

ID   Col1   Col2   Col3
--   ----   ----   ----
1       3     34     76  
2      32    976     24
3       7    235      3
4     245      1    792

¿Cuál es la mejor manera de escribir la consulta que arroja el siguiente resultado (es decir, una que arroja la columna final, una columna que contiene los valores mínimos de Col1, Col2 y Col 3 para cada fila )?

ID   Col1   Col2   Col3  TheMin
--   ----   ----   ----  ------
1       3     34     76       3
2      32    976     24      24
3       7    235      3       3
4     245      1    792       1

ACTUALIZAR:

Para aclarar (como he dicho en los comentarios) en el escenario real la base de datos está correctamente normalizada . Estas columnas de "matriz" no están en una tabla real, sino en un conjunto de resultados que se requiere en un informe. Y el nuevo requisito es que el informe también necesita esta columna MinValue. No puedo cambiar el conjunto de resultados subyacente y, por lo tanto, estaba buscando en T-SQL una práctica "tarjeta para salir de la cárcel".

Probé el enfoque CASE mencionado a continuación y funciona, aunque es un poco engorroso. También es más complicado de lo que se indica en las respuestas porque debe tener en cuenta el hecho de que hay dos valores mínimos en la misma fila.

De todos modos, pensé en publicar mi solución actual que, dadas mis limitaciones, funciona bastante bien. Utiliza el operador UNPIVOT:

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

Diré de antemano que no espero que esto ofrezca el mejor rendimiento, pero dadas las circunstancias (no puedo rediseñar todas las consultas solo para el nuevo requisito de la columna MinValue), es una forma bastante elegante de "salir de la cárcel tarjeta".


11
En mi humilde opinión, la solución UNPIVOT del autor es superior a las otras respuestas.
Joe Harris

2
Encuentro que la solución de Nizam es la solución más sencilla, incluso si me tomó un tiempo comenzar a comprenderla. Esbelto y muy utilizable.
Patrick Honorez

Respuestas:


59

Es probable que haya muchas formas de lograrlo. Mi sugerencia es usar Case / When para hacerlo. Con 3 columnas, no está tan mal.

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

6
Este fue mi pensamiento inicial. Pero la consulta real necesita 5 columnas y la cantidad de columnas podría crecer. Así que el enfoque CASE se vuelve un poco difícil de manejar. Pero funciona.
stucampbell

2
Si la cantidad de columnas puede crecer, definitivamente lo está haciendo mal: vea mi publicación (la perorata sobre por qué no debería tener su esquema de base de datos configurado de esta manera :-).
paxdiablo

2
Gracias. Como mencioné en otro comentario. No estoy consultando tablas reales. Las tablas están normalizadas correctamente. Esta consulta es parte de una consulta particularmente compleja y está trabajando en resultados intermedios de tablas derivadas.
stucampbell

2
En ese caso, ¿puede derivarlos de manera diferente para que se vean normalizados?
Kev

3
Un complemento a la respuesta de @Gmastros cuando me encontré con el problema de que algunos de los Cols tenían datos coincidentes, así que tuve que agregar el signo =. Mis datos también tenían el potencial de ser nulos, por lo que tuve que agregar la declaración o para dar cuenta de eso. Quizás haya una forma más fácil de hacer esto, pero no he encontrado una en los últimos 6 meses que he estado buscando. Gracias a todos los involucrados aquí. Seleccione Id, CaseWhen (Col1 <= Col2 OR Col2 es nulo) Y (Col1 <= Col3 OR Col3 es nulo) Entonces Col1 Cuando (Col2 <= Col1 OR Col1 es nulo) Y (Col2 <= Col3 OR Col3 es nulo) Entonces Col2 Else Col3 Termina como TheMin de YourTableNameHere
Chad Portman

55

Usando CROSS APPLY:

SELECT ID, Col1, Col2, Col3, MinValue
FROM YourTable
CROSS APPLY (SELECT MIN(d) AS MinValue FROM (VALUES (Col1), (Col2), (Col3)) AS a(d)) A

Violín SQL


Parece interesante pero no puedo hacer que esto funcione. ¿Podrías quizás ser un poco explícito? thx
Patrick Honorez

2
@iDevlop Inserté el violín SQL en mi respuesta
Nizam

Lo que no sabía era la función escalar. Parece que su respuesta también funciona sin el cross apply. ¿Agrega valor / desempeño? stackoverflow.com/a/14712024/78522
Patrick Honorez

@iDevlop Si no ofrece rendimiento, aumenta la capacidad de lectura. Por ejemplo, podría usar algo como where MinValue > 10, de lo que no podría prescindirCROSS APPLY
Nizam

2
de hecho, tuve la oportunidad de comprender el beneficio de la "reutilización" mientras tanto. Gracias. Aprendí 2 cosas hoy ;-)
Patrick Honorez

31
SELECT ID, Col1, Col2, Col3, 
    (SELECT MIN(Col) FROM (VALUES (Col1), (Col2), (Col3)) AS X(Col)) AS TheMin
FROM Table

1
Gracias por la captura. Me había perdido esa etiqueta. De hecho, no lo sé y tampoco tengo la capacidad de probarlo. Será más diligente en la verificación de etiquetas en el futuro.
dsz

3
Sin duda, la solución más elegante, no estoy seguro de por qué no tiene más votos a favor.
jwolf

Para cálculos máximos / mínimos en línea, esta es, con mucho, la mejor manera de hacer esto
Saxman

Maravillosa solución.
Phani

16

En MySQL, use esto:

select least(col1, col2, col3) FROM yourtable

Puede que no sea una declaración SQL.
Tino Jose Thannippara

4
pero en algunos casos lo es. para aquellos, esta es una respuesta maravillosa
Kirby


1
Esta extensión SQL no estándar es compatible con casi todas las bases de datos excepto el servidor SQL de Microsoft.
Mikko Rantalainen

LEASTfunciona en la última versión de instancias administradas de Microsoft SQL Server, desde hace unos 12 días. reddit.com/r/SQLServer/comments/k0dj2r/…
John Zabroski

10

Puede utilizar el enfoque de "fuerza bruta" con un giro:

SELECT CASE
    WHEN Col1 <= Col2 AND Col1 <= Col3 THEN Col1
    WHEN                  Col2 <= Col3 THEN Col2
    ELSE                                    Col3
END AS [Min Value] FROM [Your Table]

Cuando falla la primera condición when, garantiza que Col1 no es el valor más pequeño, por lo que puede eliminarlo del resto de condiciones. Así mismo para condiciones posteriores. Para cinco columnas, su consulta se convierte en:

SELECT CASE
    WHEN Col1 <= Col2 AND Col1 <= Col3 AND Col1 <= Col4 AND Col1 <= Col5 THEN Col1
    WHEN                  Col2 <= Col3 AND Col2 <= Col4 AND Col2 <= Col5 THEN Col2
    WHEN                                   Col3 <= Col4 AND Col3 <= Col5 THEN Col3
    WHEN                                                    Col4 <= Col5 THEN Col4
    ELSE                                                                      Col5
END AS [Min Value] FROM [Your Table]

Tenga en cuenta que si hay un empate entre dos o más columnas, se <=asegura de que salgamos de la CASEdeclaración lo antes posible.


2
Use en su <=lugar, de lo contrario, se usará el último valor mínimo coincidente en lugar del primero.
chezy525

6

Si las columnas fueran enteros como en su ejemplo, crearía una función:

create function f_min_int(@a as int, @b as int) 
returns int
as
begin
    return case when @a < @b then @a else coalesce(@b,@a) end
end

luego, cuando necesite usarlo, haría:

select col1, col2, col3, dbo.f_min_int(dbo.f_min_int(col1,col2),col3)

si tiene 5 columnas, lo anterior se convierte en

select col1, col2, col3, col4, col5,
dbo.f_min_int(dbo.f_min_int(dbo.f_min_int(dbo.f_min_int(col1,col2),col3),col4),col5)

4
Dado el rendimiento ridículamente malo de las funciones escalares en MSSQL, me siento obligado a desaconsejar este enfoque. Si toma este camino, al menos escriba una función que tome las 5 columnas como parámetros a la vez.
Seguirá

La recursividad reducirá el rendimiento. Pero cumplirá con el requisito.
Tino Jose Thannippara

6

La mejor manera de hacerlo es probablemente no hacerlo; es extraño que la gente insista en almacenar sus datos de una manera que requiera "gimnasia" SQL para extraer información significativa, cuando hay formas mucho más fáciles de lograr el resultado deseado si solo estructura tu esquema un poco mejor :-)

El derecho forma de hacer esto, en mi opinión, es tener la siguiente tabla:

ID    Col    Val
--    ---    ---
 1      1      3
 1      2     34
 1      3     76

 2      1     32
 2      2    976
 2      3     24

 3      1      7
 3      2    235
 3      3      3

 4      1    245
 4      2      1
 4      3    792

con ID/Colcomo clave principal (y posiblementeCol como clave adicional, según sus necesidades). Luego, su consulta se vuelve simple select min(val) from tbly aún puede tratar las 'columnas antiguas' individuales por separado al usarlas where col = 2en sus otras consultas. Esto también permite una fácil expansión en caso de que aumente el número de "columnas antiguas".

Esto hace que sus consultas de manera mucho más fácil. La pauta general que suelo utilizar es, si alguna vez tiene algo que se parece a una matriz en una fila de la base de datos, probablemente esté haciendo algo mal y debería pensar en reestructurar los datos.


Sin embargo, si por alguna razón no puede cambiar esas columnas, le sugiero que use los activadores de inserción y actualización y agregue otra columna en la que estos activadores se establezcan al mínimo Col1/2/3. Esto moverá el 'costo' de la operación de la selección a la actualización / inserción a la que pertenece: la mayoría de las tablas de bases de datos en mi experiencia se leen con mucha más frecuencia que las escritas, por lo que incurrir en el costo de escritura tiende a ser más eficiente con el tiempo.

En otras palabras, el mínimo para una fila solo cambia cuando cambia una de las otras columnas, por lo que es entonces cuando debe calcularlo, no cada vez que selecciona (lo cual se desperdicia si los datos no cambian). Luego terminarías con una tabla como:

ID   Col1   Col2   Col3   MinVal
--   ----   ----   ----   ------
 1      3     34     76        3
 2     32    976     24       24
 3      7    235      3        3
 4    245      1    792        1

Cualquier otra opción que tenga que tomar decisiones en el selectmomento suele ser una mala idea en cuanto al rendimiento, ya que los datos solo cambian al insertar / actualizar: la adición de otra columna ocupa más espacio en la base de datos y será un poco más lenta para las inserciones y actualizaciones, pero puede ser mucho más rápido para las selecciones: el enfoque preferido debería depender de sus prioridades allí, pero, como se indicó, la mayoría de las tablas se leen con mucha más frecuencia de lo que se escriben.


18
Um. Gracias por la diatriba. La base de datos real está correctamente normalizada. Este fue un ejemplo simple. La consulta real es complicada y las 5 columnas que me interesan son resultados intermedios de tablas derivadas.
stucampbell

3
La diatriba sigue en pie, lamentablemente. Hacer tablas intermedias de la forma que sugiere es tan problemático como hacer tablas permanentes como esa. Esto se evidencia por el hecho de que tienes que realizar lo que me gusta llamar gimnasia SQL para obtener el resultado que deseas.
paxdiablo

Si hay razones genuinas para necesitar la 'matriz' en una sola fila, no dude en aclararnos, pero usarla para seleccionar el valor mínimo no es una de ellas.
paxdiablo

2
+1 para que la sugerencia de activación conserve la estructura de la tabla original (si tiene fallas).
Scott Ferguson

1
¿Qué sucede si se trata de una tabla de jerarquía, unida a ella misma?
Nathan Tregillus

5

También puede hacer esto con una consulta de unión. A medida que aumenta el número de columnas, deberá modificar la consulta, pero al menos sería una modificación sencilla.

Select T.Id, T.Col1, T.Col2, T.Col3, A.TheMin
From   YourTable T
       Inner Join (
         Select A.Id, Min(A.Col1) As TheMin
         From   (
                Select Id, Col1
                From   YourTable

                Union All

                Select Id, Col2
                From   YourTable

                Union All

                Select Id, Col3
                From   YourTable
                ) As A
         Group By A.Id
       ) As A
       On T.Id = A.Id

2
Esto funciona, pero el rendimiento se degradará cuando aumente el número de filas.
Tomalak

1
Gracias. Sí, esto funciona. Como dice Tomalak, en mi consulta de palabras reales esto sería bastante desagradable para el rendimiento. Pero +1 por esfuerzo. :)
stucampbell

4

Esto es fuerza bruta pero funciona

 select case when col1 <= col2 and col1 <= col3 then col1
           case when col2 <= col1 and col2 <= col3 then col2
           case when col3 <= col1 and col3 <= col2 then col3
    as 'TheMin'
           end

from Table T

... porque min () funciona solo en una columna y no entre columnas.


También tiende a ser el más rápido ya que MIN crea una unión de bucle anidada implícita.
John Zabroski

2

Tanto esta pregunta Y esta pregunta trata de responder a esta.

La recapitulación es que Oracle tiene una función incorporada para esto, con Sql Server está atascado ya sea definiendo una función definida por el usuario o usando declaraciones de casos.


2

Para varias columnas, es mejor usar una instrucción CASE, sin embargo, para dos columnas numéricas i y j, puede usar matemáticas simples:

min (i, j) = (i + j) / 2 - abs (ij) / 2

Esta fórmula se puede usar para obtener el valor mínimo de varias columnas, pero es realmente complicado después de 2, min (i, j, k) sería min (i, min (j, k))


1

Si puede crear un procedimiento almacenado, podría tomar una matriz de valores, y podría simplemente llamar a eso.


Oracle tiene una función llamada MENOS () que hace exactamente lo que desea.
Kev

Gracias por frotar eso :) ¡No puedo creer que SQL Server no tenga un equivalente!
stucampbell

Incluso iba a decir: "Oye, mi pgsql favorito tampoco lo tiene", pero en realidad sí. ;) Sin embargo, la función en sí no sería difícil de escribir.
Kev

Oh, excepto que T-SQL ni siquiera tiene soporte para matrices (???) Bueno, supongo que podría tener una función de cinco parámetros y si necesita más, simplemente extiéndala ...
Kev

1
select *,
case when column1 < columnl2 And column1 < column3 then column1
when columnl2 < column1 And columnl2 < column3 then columnl2
else column3
end As minValue
from   tbl_example

1
Este es un duplicado de la respuesta de G Mastros, así que si se pregunta: supongo que de ahí proviene el voto negativo.
Tomalak

1

Un pequeño giro en la consulta sindical:

DECLARE @Foo TABLE (ID INT, Col1 INT, Col2 INT, Col3 INT)

INSERT @Foo (ID, Col1, Col2, Col3)
VALUES
(1, 3, 34, 76),
(2, 32, 976, 24),
(3, 7, 235, 3),
(4, 245, 1, 792)

SELECT
    ID,
    Col1,
    Col2,
    Col3,
    (
        SELECT MIN(T.Col)
        FROM
        (
            SELECT Foo.Col1 AS Col UNION ALL
            SELECT Foo.Col2 AS Col UNION ALL
            SELECT Foo.Col3 AS Col 
        ) AS T
    ) AS TheMin
FROM
    @Foo AS Foo

1

Si usa SQL 2005, puede hacer algo como esto:

;WITH    res
          AS ( SELECT   t.YourID ,
                        CAST(( SELECT   Col1 AS c01 ,
                                        Col2 AS c02 ,
                                        Col3 AS c03 ,
                                        Col4 AS c04 ,
                                        Col5 AS c05
                               FROM     YourTable AS cols
                               WHERE    YourID = t.YourID
                             FOR
                               XML AUTO ,
                                   ELEMENTS
                             ) AS XML) AS colslist
               FROM     YourTable AS t
             )
    SELECT  YourID ,
            colslist.query('for $c in //cols return min(data($c/*))').value('.',
                                            'real') AS YourMin ,
            colslist.query('for $c in //cols return avg(data($c/*))').value('.',
                                            'real') AS YourAvg ,
            colslist.query('for $c in //cols return max(data($c/*))').value('.',
                                            'real') AS YourMax
    FROM    res

De esta forma no te perderás en tantos operadores :)

Sin embargo, esto podría ser más lento que la otra opción.

Es tu elección...


Bueno, como dije, esto podría ser lento, pero si tiene demasiadas columnas (¡obviamente, como resultado de un diseño de base de datos realmente malo!), Podría valer la pena usar esto (al menos para AVG). No me diste ninguna pista si es una buena vaca sagrada o mala :) Tal vez debería usar el voto a favor / en contra para ayudarme a averiguarlo.
leoinfo

No fue ni bueno ni malo;). No soy un experto en bases de datos, así que solo estaba diciendo "vaca sagrada" porque la pregunta parecía tener una respuesta trivial. ¡Supongo que es bueno ya que lograste proporcionar una solución flexible y extensible al problema!
dreamlax

1

A continuación, utilizo una tabla temporal para obtener el mínimo de varias fechas. La primera tabla temporal consulta varias tablas unidas para obtener varias fechas (así como otros valores para la consulta), la segunda tabla temporal luego obtiene las distintas columnas y la fecha mínima utilizando tantos pases como columnas de fecha.

Esto es esencialmente como la consulta de unión, se requiere la misma cantidad de pases, pero puede ser más eficiente (según la experiencia, pero necesitaría pruebas). La eficiencia no fue un problema en este caso (8.000 registros). Se podría indexar, etc.

--==================== this gets minimums and global min
if object_id('tempdb..#temp1') is not null
    drop table #temp1
if object_id('tempdb..#temp2') is not null
    drop table #temp2

select r.recordid ,  r.ReferenceNumber, i.InventionTitle, RecordDate, i.ReceivedDate
, min(fi.uploaddate) [Min File Upload], min(fi.CorrespondenceDate) [Min File Correspondence]
into #temp1
from record r 
join Invention i on i.inventionid = r.recordid
left join LnkRecordFile lrf on lrf.recordid = r.recordid
left join fileinformation fi on fi.fileid = lrf.fileid
where r.recorddate > '2015-05-26'
 group by  r.recordid, recorddate, i.ReceivedDate,
 r.ReferenceNumber, i.InventionTitle



select recordid, recorddate [min date]
into #temp2
from #temp1

update #temp2
set [min date] = ReceivedDate 
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.ReceivedDate < [min date] and  t1.ReceivedDate > '2001-01-01'

update #temp2 
set [min date] = t1.[Min File Upload]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.[Min File Upload] < [min date] and  t1.[Min File Upload] > '2001-01-01'

update #temp2
set [min date] = t1.[Min File Correspondence]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.[Min File Correspondence] < [min date] and t1.[Min File Correspondence] > '2001-01-01'


select t1.*, t2.[min date] [LOWEST DATE]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
order by t1.recordid

1
SELECT [ID],
            (
                SELECT MIN([value].[MinValue])
                FROM
                (
                    VALUES
                        ([Col1]),
                        ([Col1]),
                        ([Col2]),
                        ([Col3])
                ) AS [value] ([MinValue])
           ) AS [MinValue]
FROM Table;

0

Si sabe qué valores está buscando, generalmente un código de estado, lo siguiente puede ser útil:

select case when 0 in (PAGE1STATUS ,PAGE2STATUS ,PAGE3STATUS,
PAGE4STATUS,PAGE5STATUS ,PAGE6STATUS) then 0 else 1 end
FROM CUSTOMERS_FORMS

0

Sé que esa pregunta es vieja, pero todavía necesitaba la respuesta y no estaba contento con otras respuestas, así que tuve que idear la mía propia, que es un giro en la respuesta de @paxdiablo .


Vengo de la tierra de SAP ASE 16.0, y solo necesitaba echar un vistazo a las estadísticas de ciertos datos que, en mi humilde opinión, están almacenados válidamente en diferentes columnas de una sola fila (representan diferentes momentos: cuando se planeó la llegada de algo, lo que se esperaba cuando la acción comenzó y finalmente cuál fue la hora real). Por lo tanto, había transpuesto columnas a las filas de la tabla temporal y realicé mi consulta sobre esto como de costumbre.

NB ¡ No es la solución única para todos!

CREATE TABLE #tempTable (ID int, columnName varchar(20), dataValue int)

INSERT INTO #tempTable 
  SELECT ID, 'Col1', Col1
    FROM sourceTable
   WHERE Col1 IS NOT NULL
INSERT INTO #tempTable 
  SELECT ID, 'Col2', Col2
    FROM sourceTable
   WHERE Col2 IS NOT NULL
INSERT INTO #tempTable 
  SELECT ID, 'Col3', Col3
    FROM sourceTable
   WHERE Col3 IS NOT NULL

SELECT ID
     , min(dataValue) AS 'Min'
     , max(dataValue) AS 'Max'
     , max(dataValue) - min(dataValue) AS 'Diff' 
  FROM #tempTable 
  GROUP BY ID

Esto tomó unos 30 segundos en el conjunto de origen de 630000 filas y usó solo datos de índice, por lo que no es lo que debe ejecutarse en un proceso de tiempo crítico, pero para cosas como la inspección de datos única o el informe al final del día, podría ser bien (pero verifique esto con sus compañeros o superiores, ¡por favor!). La principal ventaja de este estilo para mí. era que podía usar más / menos columnas y cambiar la agrupación, el filtrado, etc., especialmente una vez que se copiaron los datos.

Los datos adicionales ( columnName, maxes, ...) fueron para ayudarme en mi búsqueda, por lo que es posible que no los necesite; Los dejé aquí para quizás suscitar algunas ideas :-).

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.