Quelle est la façon la plus efficace d'obtenir le minimum de plusieurs colonnes sur SQL Server 2005?


29

Je suis dans une situation où je veux obtenir la valeur minimale de 6 colonnes.

J'ai trouvé jusqu'à présent trois façons d'accomplir cela, mais je suis préoccupé par les performances de ces méthodes et j'aimerais savoir laquelle serait la meilleure pour les performances.

La première méthode consiste à utiliser une grande déclaration de cas . Voici un exemple avec 3 colonnes, basé sur l'exemple du lien ci-dessus. Ma déclaration de cas serait beaucoup plus longue car je regarderai 6 colonnes.

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

La deuxième option consiste à utiliser l' UNIONopérateur avec plusieurs instructions de sélection . Je mettrais cela dans un UDF qui accepte un paramètre Id.

select Id, dbo.GetMinimumFromMyTable(Id)
from MyTable

et

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

Et la troisième option que j'ai trouvée était d' utiliser l'opérateur UNPIVOT , dont je ne savais même pas qu'il existait jusqu'à présent

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

En raison de la taille de la table et de la fréquence à laquelle cette table est interrogée et mise à jour, je suis préoccupé par l'impact sur les performances que ces requêtes auraient sur la base de données.

Cette requête sera en fait utilisée dans une jointure à une table avec quelques millions d'enregistrements, mais les enregistrements retournés seront réduits à une centaine d'enregistrements à la fois. Il sera exécuté plusieurs fois au cours de la journée et les 6 colonnes que j'interroge sont fréquemment mises à jour (elles contiennent des statistiques quotidiennes). Je ne pense pas qu'il y ait d'index sur les 6 colonnes que j'interroge.

Laquelle de ces méthodes est meilleure pour les performances lorsque vous essayez d'obtenir le minimum de plusieurs colonnes? Ou existe-t-il une autre meilleure méthode que je ne connais pas?

J'utilise SQL Server 2005

Exemples de données et de résultats

Si mes données contenaient des enregistrements comme celui-ci:

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

Le résultat final devrait être

Valeur ID
dix
2 2
3 1
4 4

Réponses:


22

J'ai testé les performances des 3 méthodes, et voici ce que j'ai trouvé:

  • 1 enregistrement: Pas de différence notable
  • 10 enregistrements: pas de différence notable
  • 1000 enregistrements: pas de différence notable
  • 10 000 enregistrements: la UNIONsous - requête a été un peu plus lente. La CASE WHENrequête est un peu plus rapide que UNPIVOTcelle.
  • 100 000 enregistrements: la UNIONsous-requête est beaucoup plus lente, mais la UNPIVOTrequête devient un peu plus rapide que la CASE WHENrequête
  • 500 000 enregistrements: la UNIONsous - requête est encore beaucoup plus lente, mais UNPIVOTdevient beaucoup plus rapide que la CASE WHENrequête

Donc, les résultats finaux semblent être

  • Avec des jeux d'enregistrements plus petits, il ne semble pas y avoir suffisamment de différence. Utilisez ce qui est le plus facile à lire et à entretenir.

  • Une fois que vous commencez à entrer dans des jeux d'enregistrements plus importants, la UNION ALLsous - requête commence à mal fonctionner par rapport aux deux autres méthodes.

  • L' CASEinstruction fonctionne le mieux jusqu'à un certain point (dans mon cas, environ 100 000 lignes) et le point auquel la UNPIVOTrequête devient la requête la plus performante

Le nombre réel auquel une requête devient meilleure qu'une autre changera probablement en raison de votre matériel, du schéma de base de données, des données et de la charge actuelle du serveur, alors assurez-vous de tester avec votre propre système si vous êtes préoccupé par les performances.

J'ai également effectué des tests en utilisant la réponse de Mikael ; cependant, il était plus lent que les 3 autres méthodes essayées ici pour la plupart des tailles de jeux d'enregistrements. La seule exception était qu'il faisait mieux qu'une UNION ALLrequête pour de très grandes tailles de jeux d'enregistrements. J'aime le fait qu'il montre le nom de la colonne en plus de la plus petite valeur.

Je ne suis pas un dba, donc je n'ai peut-être pas optimisé mes tests et manqué quelque chose. Je testais avec les données réelles réelles, donc cela peut avoir affecté les résultats. J'ai essayé de tenir compte de cela en exécutant chaque requête à plusieurs reprises, mais on ne sait jamais. Je serais certainement intéressé si quelqu'un écrivait un test clair de cela et partageait ses résultats.


6

Je ne sais pas ce qui est le plus rapide mais vous pouvez essayer quelque chose comme ça.

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

Résultat:

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

Si vous n'êtes pas intéressé par la colonne qui a la valeur min, vous pouvez l'utiliser à la place.

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

Une requête non pivot simplifiée.

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

6

Ajoutez une colonne calculée persistante qui utilise une CASEinstruction pour effectuer la logique dont vous avez besoin.

La valeur minimale sera alors toujours disponible efficacement lorsque vous aurez besoin de faire une jointure (ou autre) en fonction de cette valeur.

La valeur sera recalculée chaque fois que l'une des valeurs source change ( INSERT/ UPDATE/ MERGE). Je ne dis pas que c'est nécessairement la meilleure solution pour la charge de travail, je la propose simplement comme une solution, tout comme les autres réponses. Seul l'OP peut déterminer celui qui convient le mieux à la charge de travail.


1

Déclaration de cas pour 6 dates. Pour en faire moins, copiez la vraie branche de la première instruction case. Le pire des cas est lorsque Date1 est la valeur la plus basse, le meilleur des cas est lorsque Date6 est la valeur la plus basse, alors mettez la date la plus probable dans Date6. J'ai écrit cela en raison des limites des colonnes calculées.

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 vous êtes tombé sur cette page en cherchant simplement à comparer les dates et que vous n'êtes pas aussi préoccupé par les performances ou la compatibilité, vous pouvez utiliser un constructeur de valeur de table, qui peut être utilisé partout où les sous-sélections sont autorisées (SQL Server 2008 et versions ultérieures):

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

1

Votre casedéclaration n'est pas efficace. Vous faites 5 comparaisons dans le pire des cas et 2 dans le meilleur des cas; alors que trouver le minimum de ndevrait suffire dans la plupart des n-1comparaisons.

Pour chaque ligne, vous effectuez en moyenne 3,5 comparaisons au lieu de 2. Ainsi, cela prend plus de temps processeur et est lent. Essayez à nouveau vos tests en utilisant l' caseinstruction ci-dessous . Il utilise simplement 2 comparaisons par ligne et devrait être plus efficace que unpivotet 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

La union allméthode est incorrecte dans votre cas, car vous obtenez la valeur minimale non pas par ligne, mais pour la table entière. De plus, il ne sera pas efficace car vous allez scanner le même tableau 3 fois. Lorsque la table est petite, les E / S ne feront pas beaucoup de différence, mais pour les grandes tables, elles le feront. N'utilisez pas cette méthode.

Unpivotest bon et essayez également de débloquer manuellement en utilisant cross join avec votre table (select 1 union all select 2 union all select 3). Il devrait être aussi efficace que le unpivot.

La meilleure solution serait d'avoir une colonne persistante calculée, si vous n'avez pas de problèmes d'espace. Cela augmentera la taille de la ligne de 4 octets (je suppose que vous aurez du inttype), ce qui augmentera à son tour la taille de la table.

Cependant, l'espace et la mémoire sont des problèmes dans votre système et le CPU n'est pas alors ne le faites pas persister mais utilisez une colonne calculée simple en utilisant l'instruction case. Cela rendra le code plus simple.


-1

Je suppose que la première option est la plus rapide (bien qu'elle ne semble pas très élégante du point de vue de la programmation!). En effet, il traite exactement N lignes (où N est la taille du tableau) et ne doit pas effectuer de recherche ni de tri comme la méthode 2 ou 3.

Un test avec un grand échantillon devrait prouver le point.

Encore une autre option à considérer (comme si vous en avez besoin de plus!), Est de créer une vue matérialisée sur votre table. si la taille de votre table est de 100s de milliers ou plus. De cette façon, la valeur min est calculée lorsque la ligne est modifiée et la table entière ne devrait pas être traitée avec chaque requête. Dans SQL Server, les vues matérialisées sont appelées vues indexées


-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

Vous ne tenez pas compte des valeurs NULL - ce qui rend votre expression CASE relativement simple. Cependant, si au moins une des colonnes est bien NULL, votre solution retournera Year1comme résultat, ce qui n'est pas nécessairement correct.
Andriy M
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.