web-dev-qa-db-fra.com

Quel est le meilleur moyen de sélectionner la valeur minimale dans plusieurs colonnes?

Étant donné le tableau suivant dans SQL Server 2005:

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

Quelle est la meilleure façon d’écrire la requête qui donne le résultat suivant (c’est-à-dire celle qui donne la dernière colonne - une colonne contenant les valeurs minimales de Col1, Col2 et Col 3 pour chaque ligne)?

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

UPDATE:

Pour plus de clarté (comme je l’ai dit dans les commentaires), dans le scénario réel, la base de données est correctement normalisée. Ces colonnes "tableau" ne figurent pas dans une table réelle mais dans un ensemble de résultats requis dans un rapport. Et la nouvelle exigence est que le rapport a également besoin de cette colonne MinValue. Je ne peux pas changer le jeu de résultats sous-jacent et je me suis donc tourné vers T-SQL pour obtenir une "carte de sortie de prison" pratique.

J'ai essayé l'approche CASE mentionnée ci-dessous et cela fonctionne, même si c'est un peu lourd. C'est également plus compliqué que ce qui est indiqué dans les réponses, car vous devez tenir compte du fait qu'il y a deux valeurs min dans la même ligne.

En tout cas, je pensais publier ma solution actuelle qui, compte tenu de mes contraintes, fonctionne plutôt bien. Il utilise l'opérateur 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

Je dirai d'emblée que je ne m'attends pas à ce que cela offre la meilleure performance, mais vu les circonstances (je ne peux pas repenser toutes les requêtes uniquement pour la nouvelle exigence de la colonne MinValue), il s'agit d'un assez élégant "sortir de prison" carte".

64
stucampbell

Il y a probablement plusieurs façons de le faire. Ma suggestion est d'utiliser Cas/Quand le faire. Avec 3 colonnes, c'est pas trop grave.

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
48
G Mastros

En utilisant CROSS APPLY:

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

SQL Fiddle

43
Nizam
SELECT ID, Col1, Col2, Col3, 
    (SELECT MIN(Col) FROM (VALUES (Col1), (Col2), (Col3)) AS X(Col)) AS TheMin
FROM Table
11
dsz

La meilleure façon de le faire est probablement de ne pas le faire - il est étrange que des personnes insistent pour que leurs données soient stockées de manière à requérir une "gymnastique" SQL extraire des informations significatives, quand il existe des moyens beaucoup plus faciles pour obtenir le résultat souhaité si vous structurez un peu mieux votre schéma :-)

Le moyen correct de le faire, à mon avis, consiste à disposer du tableau suivant:

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

avec ID/Col comme clé primaire (et éventuellement Col comme clé supplémentaire, selon vos besoins). Votre requête devient alors une simple select min(val) from tbl et vous pouvez toujours traiter les 'anciennes colonnes' individuelles séparément en utilisant where col = 2 Dans vos autres requêtes. Cela permet également une expansion facile si le nombre de "vieilles colonnes" augmente.

Cela rend vos requêtes donc beaucoup plus faciles. La directive générale que j’ai tendance à utiliser est que si vous avez déjà quelque chose qui ressemble à un tableau dans une ligne de la base de données, vous faites probablement quelque chose de mal et devrait penser à restructurer les données.


Cependant, si pour une raison quelconque, vous ne pouvez pas modifier ces colonnes, nous vous suggérons d'utiliser des déclencheurs d'insertion et de mise à jour et d'ajouter une autre colonne que ces déclencheurs ont réglée au minimum sur Col1/2/3. Cela déplace le "coût" de l'opération de la sélection vers la mise à jour/insertion où elle appartient - la plupart des tables de base de données de mon expérience sont lues beaucoup plus souvent qu'écrites, de sorte que les coûts d'écriture ont tendance à être plus efficaces au fil du temps.

En d’autres termes, le minimum pour une ligne ne change que lorsque l’une des autres colonnes change, donc c’est le moment où vous devriez le calculer, pas à chaque fois que vous sélectionnez (ce qui est gaspillé si les données ne changent pas). Vous vous retrouveriez alors avec une table comme celle-ci:

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

Toute autre option devant prendre des décisions à l'heure select est généralement une mauvaise idée en termes de performances, car les données ne changent que lors de l'insertion/de la mise à jour - l'ajout d'une autre colonne occupe plus d'espace dans la base de données et sera légèrement plus lent pour les insertions et les mises à jour mais peut être beaucoup plus rapide pour les sélections - l'approche recommandée devrait dépendre de vos priorités, mais, comme indiqué, la plupart des tableaux sont lire loin plus souvent qu'ils ne sont écrits.

7
paxdiablo

Vous pouvez utiliser l'approche "force brute" avec une torsion:

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

Lorsque la première condition lorsque la condition échoue, cela garantit que Col1 n'est pas la valeur la plus petite. Vous pouvez donc l'éliminer du reste des conditions. De même pour les conditions ultérieures. Votre requête devient la suivante pour cinq colonnes:

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]

Notez que s'il y a une égalité entre deux colonnes ou plus, alors <= s'assure que nous quittons l'instruction CASE le plus tôt possible.

7
Salman A

Utilisez ceci:

select least(col1, col2, col3) FROM yourtable
6
user3493139

Si les colonnes étaient des entiers comme dans votre exemple, je créerais une fonction:

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

alors quand j'ai besoin de l'utiliser je ferais:

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

si vous avez 5 colonnes alors ce qui précède devient

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)
6
Georgios

Vous pouvez également le faire avec une requête d'union. Au fur et à mesure que le nombre de colonnes augmente, vous devrez modifier la requête, mais il s'agira au moins d'une modification simple.

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
5
G Mastros

C'est la force brute mais ça marche

 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

... parce que min () ne fonctionne que sur une colonne et pas sur plusieurs colonnes.

3
Learning

Les deux cette question Et cette question essayez d'y répondre.

En résumé, Oracle possède une fonction intégrée à cet effet. Sql Server vous oblige à définir une fonction définie par l'utilisateur ou à utiliser des instructions de casse.

2
Sam Saffron

Si vous êtes capable de créer une procédure stockée, cela pourrait prendre un tableau de valeurs, et vous pourriez simplement appeler cela.

1
Kev
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
Phil Corcoran
SELECT [ID],
            (
                SELECT MIN([value].[MinValue])
                FROM
                (
                    VALUES
                        ([Col1]),
                        ([Col1]),
                        ([Col2]),
                        ([Col3])
                ) AS [value] ([MinValue])
           ) AS [MinValue]
FROM Table;
1

Si vous utilisez SQL 2005, vous pouvez faire quelque chose de bien comme ça:

;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 cette façon, vous ne vous perdez pas dans autant d'opérateurs :)

Cependant, cela pourrait être plus lent que l'autre choix.

C'est ton choix...

1
leoinfo

Pour plusieurs colonnes, il est préférable d’utiliser une instruction CASE. Toutefois, pour deux colonnes numériques i et j, vous pouvez utiliser un calcul simple:

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

Cette formule peut être utilisée pour obtenir la valeur minimale de plusieurs colonnes, mais sa valeur réellement désordonnée après 2, min (i, j, k) serait min (i, min (j, k)).

1
user3658750

Ci-dessous, j'utilise une table temporaire pour obtenir le minimum de plusieurs dates. La première table temporaire interroge plusieurs tables jointes pour obtenir différentes dates (ainsi que d'autres valeurs pour la requête). La seconde table temporaire récupère ensuite les différentes colonnes et la date minimale en utilisant autant de passes que de colonnes de date.

Cela ressemble essentiellement à la requête d'union, le même nombre de passes est requis, mais peut être plus efficace (en fonction de l'expérience, mais aurait besoin de tests). L'efficacité n'était pas un problème dans ce cas (8 000 enregistrements). On pourrait indexer 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
user3438020

Un peu de torsion sur la requête de l'union:

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
Lamprey

Si vous connaissez les valeurs que vous recherchez, généralement un code d'état, les informations suivantes peuvent être utiles:

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

Je sais que cette question est ancienne, mais j’avais toujours besoin de la réponse et n’étais pas satisfaite des autres réponses; j’ai donc dû concevoir la mienne, ce qui est un peu différent de @ paxdiablo´s answer .


Je venais du pays de SAP ASE 16.0 et je n'avais besoin que d'un aperçu des statistiques de certaines données enregistrées à mon humble avis dans différentes colonnes d'une même ligne (elles représentent des moments différents - quand l'arrivée de quelque chose était planifiée, ce à quoi on s'attendait quand l'action a commencé et finalement quelle était l'heure réelle). Ainsi, j'avais transposé des colonnes dans les lignes de la table temporaire et préformé ma requête sur cette requête comme d'habitude.

N.B. Ce n'est pas la solution universelle qui vous convient!

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

Cela prenait environ 30 secondes sur un ensemble source de 630000 lignes et utilisait uniquement des données d'index. Il n'était donc pas utile de l'exécuter dans un processus critique mais pour des tâches telles que l'inspection ponctuelle de données ou le rapport de fin de journée. bien (mais vérifiez ceci avec vos pairs ou vos supérieurs, s'il vous plaît!). Le principal avantage de ce style pour moi était que je pouvais facilement utiliser plus/moins de colonnes et modifier le groupement, le filtrage, etc., en particulier une fois les données copiées.

Les données supplémentaires (columnName, maxes, ...) devaient m'aider dans ma recherche, vous pourriez donc ne pas en avoir besoin; Je les ai laissés ici peut-être à spark quelques idées :-).

0
Rao