Quelle est la meilleure façon de sélectionner la valeur minimale de 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.-à-d. Celui qui donne la colonne finale – une colonne contenant les valeurs minium de Col1, Col2 et Col3 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 

METTRE À JOUR:

Pour clarifier (comme je l'ai dit dans les commentaires) dans le scénario réel, la database est correctement normalisée . Ces colonnes "tableau" ne se trouvent pas dans une table réelle mais dans un set de résultats requirejs 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 par conséquent je cherchais à T-SQL pour une pratique "sortir de la carte de prison".

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

Quoi qu'il en soit, j'ai pensé postr ma solution actuelle qui, count 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 dirais d'emblée que je ne m'attends pas à ce que ce soit la meilleure performance, mais étant donné les circonstances (je ne peux pas reconcevoir toutes les requêtes juste pour la nouvelle exigence de colonne MinValue), c'est une sortie élégante carte".

    Il y a probablement plusieurs façons d'accomplir cela. Ma suggestion est d'utiliser Case / When pour le faire. Avec 3 colonnes, ce n'est pas trop 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 

    Utilisation de 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

    Le meilleur moyen est de ne pas le faire – il est étrange que les gens insistent pour stocker leurs données d'une manière qui nécessite une "gymnastique" SQL pour extraire des informations significatives quand il y a des moyens plus faciles d'atteindre le résultat désiré. .

    À mon avis, la bonne façon de faire est d'avoir le 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 key primaire et éventuellement Col comme key supplémentaire, en fonction de vos besoins. Ensuite, votre requête devient simple

     select min(val) from tbl 

    et vous pouvez toujours traiter les «vieilles colonnes» 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 beaucoup plus faciles. La règle générale que j'ai tendance à utiliser est la suivante: si vous avez déjà quelque chose qui ressemble à un tableau dans une ligne de database, vous faites probablement quelque chose de mal et vous devriez penser à restructurer datatables.


    Cependant, si pour une raison quelconque vous ne pouvez pas changer ces colonnes, je suggérerais d'utiliser des triggersurs d'insertion et de mise à jour et d'append une autre colonne que ces triggersurs définiraient au minimum sur Col1/2/3 . Cela va déplacer le «coût» de l'opération de la sélection à la mise à jour / insert où il appartient – la plupart des tables de database sont lues beaucoup plus souvent qu'écrites, donc le coût d'écriture tend à être plus efficace avec le time.

    En d'autres termes, le minimum pour une ligne ne change que lorsque l'une des autres colonnes change, c'est donc à ce moment que vous devez le calculer, pas chaque fois que vous select (ce qui est gaspillé si datatables ne changent pas). Vous finiriez avec une table comme:

     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 qui doit prendre des décisions au moment select est généralement une mauvaise idée en termes de performances, car datatables ne changent que lors de l'insertion / mise à jour – l'ajout d'une autre colonne occupera plus d'espace dans la BD et sera légèrement plus lent et mises à jour, mais peut être beaucoup plus rapide pour les sélections – l'approche préférée devrait dépendre de vos priorités, mais, comme indiqué, la plupart des tables sont lues beaucoup plus souvent qu'elles ne sont écrites.

    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 je dois 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 le dessus 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) 

    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 échoue, cela garantit que Col1 n'est pas la plus petite valeur, donc vous pouvez l'éliminer du rest des conditions. De même pour les conditions suivantes. Pour cinq colonnes, votre requête devient:

     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 un lien entre deux ou plusieurs colonnes alors <= assure que nous quittons l'instruction CASE plus tôt possible.

    Vous pouvez également le faire avec une requête union. À mesure que le nombre de colonnes augmente, vous devez modifier la requête, mais au less, il s'agit d'une modification directe.

     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 
     SELECT ID, Col1, Col2, Col3, (SELECT MIN(Col) FROM (VALUES (Col1), (Col2), (Col3)) AS X(Col)) AS TheMin FROM Table 

    Ceci est la force brute, mais fonctionne

      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 

    … car min () ne fonctionne que sur une colonne et pas sur les colonnes.

    Tant cette question Et cette question essaye d'y répondre.

    Le récapitulatif est qu'Oracle a une fonction embeddede pour cela, avec Sql Server vous êtes bloqué en définissant une fonction définie par l'user ou en utilisant des instructions case.

    Si vous êtes en mesure d'effectuer une procédure stockée, cela peut prendre un certain nombre de valeurs, et vous pourriez simplement appeler cela.

     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 

    Une petite torsion sur la requête 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 

    Si vous utilisez SQL 2005, vous pouvez faire quelque chose de joli comme ceci:

     ;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 tant d'opérateurs 🙂

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

    C'est ton choix…

    Ci-dessous, j'utilise une table temporaire pour get le minimum de plusieurs dates. La première table temporaire interroge plusieurs tables jointes pour get 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 dates.

    C'est essentiellement comme la requête union, le même nombre de passes est requirejs, mais peut être plus efficace (basé sur l'expérience, mais aurait besoin d'être testé). L'efficacité n'était pas un problème dans ce cas (8 000 loggings). 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 

    Si vous connaissez les valeurs que vous searchz, généralement un code d'état, les éléments suivants peuvent vous être utiles:

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

    Utilisez ceci:

     select least(col1, col2, col3) FROM yourtable 

    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 des calculs simples:

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

    Cette formule peut être utilisée pour get la valeur minimale de plusieurs colonnes, mais son passé vraiment malpropre 2, min (i, j, k) serait min (i, min (j, k))

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